170 lines
6.0 KiB
Swift
170 lines
6.0 KiB
Swift
//
|
|
// 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 }
|
|
}
|
|
}
|