// // AnimatedBackground.swift // SportsTime // // Animated sports background with floating icons and route lines. // Used by ThemedBackground modifier when animations are enabled. // import SwiftUI // MARK: - Animated Sports Background /// Floating sports icons with route lines and subtle glow effects struct AnimatedSportsBackground: View { @Environment(\.colorScheme) private var colorScheme @State private var animate = false var body: some View { ZStack { // Base gradient Theme.backgroundGradient(colorScheme) // Route lines with city dots (subtle background element) RouteMapLayer(animate: animate) // Floating sports icons with gentle glow ForEach(0..<20, id: \.self) { index in AnimatedSportsIcon(index: index, animate: animate) } } .onAppear { withAnimation(.easeInOut(duration: 5.0).repeatForever(autoreverses: true)) { animate = true } } } } // MARK: - Route Map Layer /// Background route lines connecting city dots (very subtle) struct RouteMapLayer: View { @Environment(\.colorScheme) private var colorScheme let animate: Bool var body: some View { Canvas { context, size in // City points scattered across the view let points: [CGPoint] = [ CGPoint(x: size.width * 0.1, y: size.height * 0.15), CGPoint(x: size.width * 0.3, y: size.height * 0.25), CGPoint(x: size.width * 0.55, y: size.height * 0.1), CGPoint(x: size.width * 0.75, y: size.height * 0.3), CGPoint(x: size.width * 0.2, y: size.height * 0.45), CGPoint(x: size.width * 0.6, y: size.height * 0.5), CGPoint(x: size.width * 0.85, y: size.height * 0.2), CGPoint(x: size.width * 0.4, y: size.height * 0.65), CGPoint(x: size.width * 0.8, y: size.height * 0.6), CGPoint(x: size.width * 0.15, y: size.height * 0.75), CGPoint(x: size.width * 0.5, y: size.height * 0.8), CGPoint(x: size.width * 0.9, y: size.height * 0.85), ] // Draw dotted route lines connecting points let routePairs: [(Int, Int)] = [ (0, 1), (1, 3), (3, 6), (2, 6), (1, 4), (4, 5), (5, 8), (4, 9), (5, 7), (7, 10), (9, 10), (10, 11), (2, 3), (8, 11) ] let lineColor = Theme.warmOrange.resolve(in: .init()) for (start, end) in routePairs { var path = Path() path.move(to: points[start]) path.addLine(to: points[end]) context.stroke( path, with: .color(Color(lineColor).opacity(0.05)), style: StrokeStyle(lineWidth: 1, dash: [5, 5]) ) } // Draw city dots (very subtle) for (index, point) in points.enumerated() { let isMainCity = index % 4 == 0 let dotSize: CGFloat = isMainCity ? 5 : 3 let dotPath = Path(ellipseIn: CGRect( x: point.x - dotSize / 2, y: point.y - dotSize / 2, width: dotSize, height: dotSize )) context.fill(dotPath, with: .color(Color(lineColor).opacity(isMainCity ? 0.1 : 0.05))) } } } } // MARK: - Animated Sports Icon /// Individual floating sports icon with subtle glow animation struct AnimatedSportsIcon: View { let index: Int let animate: Bool @State private var glowOpacity: Double = 0 private let configs: [(x: CGFloat, y: CGFloat, icon: String, rotation: Double, scale: CGFloat)] = [ // Edge icons (0.06, 0.08, "football.fill", -15, 0.85), (0.94, 0.1, "basketball.fill", 12, 0.8), (0.04, 0.28, "baseball.fill", 8, 0.75), (0.96, 0.32, "hockey.puck.fill", -10, 0.7), (0.08, 0.48, "soccerball", 6, 0.8), (0.92, 0.45, "figure.run", -6, 0.85), (0.05, 0.68, "sportscourt.fill", 4, 0.75), (0.95, 0.65, "trophy.fill", -12, 0.8), (0.1, 0.88, "ticket.fill", 10, 0.7), (0.9, 0.85, "mappin.circle.fill", -8, 0.75), (0.5, 0.03, "car.fill", 0, 0.7), (0.5, 0.97, "map.fill", 3, 0.75), (0.75, 0.95, "flag.checkered", 7, 0.7), // Middle area icons (will appear behind cards) (0.35, 0.22, "tennisball.fill", -8, 0.65), (0.65, 0.35, "volleyball.fill", 10, 0.6), (0.3, 0.52, "figure.baseball", -5, 0.65), (0.7, 0.58, "figure.basketball", 8, 0.6), (0.4, 0.72, "figure.hockey", -10, 0.65), (0.6, 0.82, "figure.soccer", 5, 0.6), ] var body: some View { let config = configs[index] GeometryReader { geo in ZStack { // Subtle glow circle behind icon when active Circle() .fill(Theme.warmOrange) .frame(width: 28 * config.scale, height: 28 * config.scale) .blur(radius: 8) .opacity(glowOpacity * 0.2) Image(systemName: config.icon) .font(.system(size: 20 * config.scale)) .foregroundStyle(Theme.warmOrange.opacity(0.08 + glowOpacity * 0.1)) .rotationEffect(.degrees(config.rotation)) } .position(x: geo.size.width * config.x, y: geo.size.height * config.y) .scaleEffect(animate ? 1.02 : 0.98) .scaleEffect(1 + glowOpacity * 0.05) .animation( .easeInOut(duration: 4.0 + Double(index) * 0.15) .repeatForever(autoreverses: true) .delay(Double(index) * 0.2), value: animate ) } .onAppear { startRandomGlow() } } private func startRandomGlow() { let initialDelay = Double.random(in: 2.0...8.0) DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) { triggerGlow() } } private func triggerGlow() { // Slow fade in withAnimation(.easeIn(duration: 0.8)) { glowOpacity = 1 } // Hold briefly then slow fade out DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { withAnimation(.easeOut(duration: 1.0)) { glowOpacity = 0 } } // Longer interval between glows let nextGlow = Double.random(in: 6.0...12.0) DispatchQueue.main.asyncAfter(deadline: .now() + nextGlow) { triggerGlow() } } }