// // SplashScreenView.swift // FluxRead // // Created by RSA on 29/11/2025. // import Foundation import SwiftUI import Combine struct SplashScreenView: View { @ObservedObject var viewModel: SplashScreenViewModel @State private var glowPulse = false @State private var logoScale = 0.85 @State private var logoOpacity = 0.0 @State private var ringRotate = 0.0 @State private var ringOpacity = 0.0 var body: some View { ZStack { // ── Deep black base ────────────────────────────────────────── Color.black.ignoresSafeArea() // ── Bottom-half warm particle field ────────────────────────── SplashParticleField() .ignoresSafeArea() .opacity(0.90) // ── Radial glow anchored to centre top ─────────────────────── VStack(spacing: 0) { RadialGradient( colors: [ Color.orange.opacity(glowPulse ? 0.22 : 0.10), Color.yellow.opacity(glowPulse ? 0.10 : 0.04), Color.clear ], center: .center, startRadius: 0, endRadius: 340 ) .frame(width: 680, height: 680) .blur(radius: 48) .scaleEffect(glowPulse ? 1.12 : 1.0) .animation( .easeInOut(duration: 3.8).repeatForever(autoreverses: true), value: glowPulse ) Spacer() } .ignoresSafeArea() // ── Orbiting arc ring around logo ──────────────────────────── ZStack { // slow outer arc Circle() .trim(from: 0.0, to: 0.28) .stroke( LinearGradient( colors: [Color.orange.opacity(0.45), Color.clear], startPoint: .leading, endPoint: .trailing ), style: StrokeStyle(lineWidth: 1.0, lineCap: .round) ) .frame(width: 210, height: 210) .rotationEffect(.degrees(ringRotate)) .opacity(ringOpacity) // faster inner arc Circle() .trim(from: 0.55, to: 0.72) .stroke( LinearGradient( colors: [Color.yellow.opacity(0.30), Color.clear], startPoint: .leading, endPoint: .trailing ), style: StrokeStyle(lineWidth: 0.6, lineCap: .round) ) .frame(width: 175, height: 175) .rotationEffect(.degrees(-ringRotate * 1.4)) .opacity(ringOpacity) } // ── Logo centred ───────────────────────────────────────────── VStack(spacing: 0) { Spacer() Spacer() } } .ignoresSafeArea() .onAppear { // Logo entrance withAnimation(.spring(response: 0.75, dampingFraction: 0.72).delay(0.1)) { logoScale = 1.0 logoOpacity = 1.0 } // Glow breath glowPulse = true // Ring fade-in then continuous rotation withAnimation(.easeIn(duration: 0.8).delay(0.4)) { ringOpacity = 1.0 } withAnimation(.linear(duration: 12).repeatForever(autoreverses: false).delay(0.4)) { ringRotate = 360 } } } } // MARK: - Splash Particle Field // Warm, slow-drifting particles — denser than the reader version, tinted orange/gold struct SplashParticleField: View { @State private var tick: Double = 0 private let timer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect() private struct Particle { let x, y, speed, phase: Double let radius: CGFloat let opacity: Double let warm: Bool // warm (orange/gold) vs cool (white) } private let particles: [Particle] = (0..<120).map { i in let seed = Double(i) * 1.618033 return Particle( x: (seed * 0.7369).truncatingRemainder(dividingBy: 1.0), y: (seed * 1.2345).truncatingRemainder(dividingBy: 1.0), speed: Double.random(in: 8...28), phase: Double.random(in: 0...Double.pi * 2), radius: CGFloat(Double.random(in: 0.7...2.8)), opacity: Double.random(in: 0.04...0.28), warm: i % 3 != 0 ) } var body: some View { Canvas { ctx, size in let t = tick for p in particles { let x = (p.x + t / p.speed).truncatingRemainder(dividingBy: 1.0) * size.width let drift = sin(t / (p.speed * 0.6) + p.phase) * 22 let y = p.y * size.height + CGFloat(drift) let pulse = 0.65 + 0.35 * sin(t / 2.5 + p.phase) let alpha = p.opacity * pulse let color: Color = p.warm ? Color(red: 1.0, green: 0.62, blue: 0.18).opacity(alpha) : Color.white.opacity(alpha * 0.55) ctx.fill( Path(ellipseIn: CGRect( x: x - p.radius, y: y - p.radius, width: p.radius * 2, height: p.radius * 2 )), with: .color(color) ) } } .onReceive(timer) { _ in tick += 1.0 / 60.0 } } }