Redesign lock screen with Emotional Aurora theme

Replace basic lock screen with immersive mood-themed design featuring:
- Animated aurora gradient background using mood colors
- Floating mood-colored particles with gentle animation
- Central breathing orb with rotating angular gradients
- Glassmorphic unlock button with pulse animation
- Elegant serif typography ("Your Feelings are safe here")
- Smooth entrance animations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-24 10:34:49 -06:00
parent e9adc14851
commit 745e226ecb

View File

@@ -3,83 +3,461 @@
// Feels
//
// Lock screen shown when privacy lock is enabled and app needs authentication.
// Design: "Emotional Aurora" - A sanctuary for your feelings
//
import SwiftUI
// MARK: - Floating Mood Particle
struct MoodParticle: Identifiable {
let id = UUID()
var x: CGFloat
var y: CGFloat
let size: CGFloat
let color: Color
let duration: Double
let delay: Double
}
// MARK: - Aurora Gradient Background
struct AuroraBackground: View {
@State private var animateGradient = false
private let moodColors: [Color] = [
Color(hex: "31d158"), // great - green
Color(hex: "ffd709"), // good - yellow
Color(hex: "0b84ff"), // average - blue
Color(hex: "ff9e0b"), // bad - orange
Color(hex: "ff453a"), // horrible - red
]
var body: some View {
ZStack {
// Base dark gradient
LinearGradient(
colors: [
Color(hex: "0a0a0f"),
Color(hex: "12121a"),
Color(hex: "0d0d14")
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
// Aurora layer 1 - green/blue
EllipticalGradient(
colors: [
moodColors[0].opacity(0.3),
moodColors[2].opacity(0.15),
.clear
],
center: .topLeading,
startRadiusFraction: 0,
endRadiusFraction: 0.8
)
.blur(radius: 60)
.offset(x: animateGradient ? 20 : -20, y: animateGradient ? -30 : 30)
// Aurora layer 2 - yellow/orange
EllipticalGradient(
colors: [
moodColors[1].opacity(0.2),
moodColors[3].opacity(0.1),
.clear
],
center: .bottomTrailing,
startRadiusFraction: 0,
endRadiusFraction: 0.7
)
.blur(radius: 80)
.offset(x: animateGradient ? -30 : 30, y: animateGradient ? 20 : -20)
// Aurora layer 3 - subtle red accent
RadialGradient(
colors: [
moodColors[4].opacity(0.15),
.clear
],
center: UnitPoint(x: 0.8, y: 0.3),
startRadius: 0,
endRadius: 200
)
.blur(radius: 40)
.offset(y: animateGradient ? -10 : 10)
// Noise texture overlay
Rectangle()
.fill(.white.opacity(0.015))
.background(
Canvas { context, size in
for _ in 0..<1000 {
let x = CGFloat.random(in: 0...size.width)
let y = CGFloat.random(in: 0...size.height)
let opacity = Double.random(in: 0.01...0.04)
context.fill(
Path(ellipseIn: CGRect(x: x, y: y, width: 1, height: 1)),
with: .color(.white.opacity(opacity))
)
}
}
)
}
.ignoresSafeArea()
.onAppear {
withAnimation(
.easeInOut(duration: 8)
.repeatForever(autoreverses: true)
) {
animateGradient = true
}
}
}
}
// MARK: - Floating Particles Layer
struct FloatingParticlesView: View {
@State private var particles: [MoodParticle] = []
private let moodColors: [Color] = [
Color(hex: "31d158"),
Color(hex: "ffd709"),
Color(hex: "0b84ff"),
Color(hex: "ff9e0b"),
Color(hex: "ff453a"),
]
var body: some View {
GeometryReader { geo in
ZStack {
ForEach(particles) { particle in
Circle()
.fill(particle.color)
.frame(width: particle.size, height: particle.size)
.blur(radius: particle.size * 0.3)
.position(x: particle.x, y: particle.y)
.modifier(FloatingAnimation(
startY: particle.y,
duration: particle.duration,
delay: particle.delay
))
}
}
.onAppear {
generateParticles(in: geo.size)
}
}
}
private func generateParticles(in size: CGSize) {
particles = (0..<15).map { _ in
MoodParticle(
x: CGFloat.random(in: 0...size.width),
y: CGFloat.random(in: 0...size.height),
size: CGFloat.random(in: 4...12),
color: moodColors.randomElement()!.opacity(Double.random(in: 0.2...0.5)),
duration: Double.random(in: 6...12),
delay: Double.random(in: 0...3)
)
}
}
}
struct FloatingAnimation: ViewModifier {
let startY: CGFloat
let duration: Double
let delay: Double
@State private var offset: CGFloat = 0
func body(content: Content) -> some View {
content
.offset(y: offset)
.onAppear {
withAnimation(
.easeInOut(duration: duration)
.repeatForever(autoreverses: true)
.delay(delay)
) {
offset = CGFloat.random(in: -30...30)
}
}
}
}
// MARK: - Central Breathing Orb
struct BreathingOrb: View {
@State private var breathe = false
@State private var rotate = false
private let moodColors: [Color] = [
Color(hex: "31d158"),
Color(hex: "ffd709"),
Color(hex: "0b84ff"),
Color(hex: "ff9e0b"),
Color(hex: "ff453a"),
]
var body: some View {
ZStack {
// Outer glow
Circle()
.fill(
AngularGradient(
colors: moodColors + [moodColors[0]],
center: .center,
startAngle: .degrees(0),
endAngle: .degrees(360)
)
)
.frame(width: 180, height: 180)
.blur(radius: 40)
.opacity(0.6)
.scaleEffect(breathe ? 1.2 : 0.9)
.rotationEffect(.degrees(rotate ? 360 : 0))
// Middle ring
Circle()
.fill(
AngularGradient(
colors: moodColors.reversed() + [moodColors.last!],
center: .center
)
)
.frame(width: 120, height: 120)
.blur(radius: 20)
.opacity(0.8)
.scaleEffect(breathe ? 1.1 : 0.95)
.rotationEffect(.degrees(rotate ? -360 : 0))
// Inner core
Circle()
.fill(
RadialGradient(
colors: [
.white.opacity(0.9),
.white.opacity(0.3),
.clear
],
center: .center,
startRadius: 0,
endRadius: 40
)
)
.frame(width: 80, height: 80)
.scaleEffect(breathe ? 1.05 : 0.98)
// Glossy highlight
Ellipse()
.fill(
LinearGradient(
colors: [.white.opacity(0.4), .clear],
startPoint: .top,
endPoint: .center
)
)
.frame(width: 50, height: 30)
.offset(y: -15)
.scaleEffect(breathe ? 1.05 : 0.98)
}
.onAppear {
withAnimation(
.easeInOut(duration: 4)
.repeatForever(autoreverses: true)
) {
breathe = true
}
withAnimation(
.linear(duration: 20)
.repeatForever(autoreverses: false)
) {
rotate = true
}
}
}
}
// MARK: - Glassmorphic Button
struct GlassButton: View {
let icon: String
let title: String
let action: () -> Void
@State private var isPressed = false
@State private var pulse = false
var body: some View {
Button(action: action) {
HStack(spacing: 14) {
ZStack {
// Pulse ring
Circle()
.stroke(lineWidth: 2)
.foregroundColor(.white.opacity(0.3))
.frame(width: 44, height: 44)
.scaleEffect(pulse ? 1.3 : 1)
.opacity(pulse ? 0 : 0.6)
// Icon background
Circle()
.fill(.white.opacity(0.15))
.frame(width: 44, height: 44)
Image(systemName: icon)
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
}
Text(title)
.font(.system(size: 17, weight: .semibold, design: .rounded))
.foregroundColor(.white)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.padding(.horizontal, 24)
.background(
ZStack {
// Glass effect
RoundedRectangle(cornerRadius: 20)
.fill(.ultraThinMaterial)
.opacity(0.8)
// Border gradient
RoundedRectangle(cornerRadius: 20)
.stroke(
LinearGradient(
colors: [
.white.opacity(0.4),
.white.opacity(0.1),
.white.opacity(0.2)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
// Inner shadow
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
colors: [.white.opacity(0.1), .clear],
startPoint: .top,
endPoint: .bottom
)
)
}
)
.scaleEffect(isPressed ? 0.97 : 1)
}
.buttonStyle(PlainButtonStyle())
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
withAnimation(.easeInOut(duration: 0.1)) {
isPressed = true
}
}
.onEnded { _ in
withAnimation(.easeInOut(duration: 0.1)) {
isPressed = false
}
}
)
.onAppear {
withAnimation(
.easeInOut(duration: 2)
.repeatForever(autoreverses: false)
) {
pulse = true
}
}
}
}
// MARK: - Main Lock Screen View
struct LockScreenView: View {
@ObservedObject var authManager: BiometricAuthManager
@State private var showError = false
@State private var showContent = false
var body: some View {
ZStack {
// Background gradient
LinearGradient(
colors: [
Color(.systemBackground),
Color(.systemGray6)
],
startPoint: .top,
endPoint: .bottom
)
.ignoresSafeArea()
// Aurora background
AuroraBackground()
VStack(spacing: 40) {
// Floating particles
FloatingParticlesView()
// Main content
VStack(spacing: 0) {
Spacer()
// App icon / lock icon
VStack(spacing: 20) {
Image(systemName: "lock.fill")
.font(.largeTitle)
.foregroundStyle(.secondary)
// Breathing orb
BreathingOrb()
.opacity(showContent ? 1 : 0)
.scaleEffect(showContent ? 1 : 0.8)
Text("Feels is Locked")
.font(.title2)
.fontWeight(.semibold)
Spacer()
.frame(height: 50)
Text("Authenticate to access your mood data")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
// Text content
VStack(spacing: 12) {
Text("Your Feelings")
.font(.system(size: 32, weight: .light, design: .serif))
.foregroundColor(.white)
Text("are safe here")
.font(.system(size: 32, weight: .ultraLight, design: .serif))
.foregroundColor(.white.opacity(0.7))
}
.opacity(showContent ? 1 : 0)
.offset(y: showContent ? 0 : 20)
Spacer()
.frame(height: 16)
Text("Authenticate to continue your journey")
.font(.system(size: 14, weight: .regular, design: .rounded))
.foregroundColor(.white.opacity(0.5))
.opacity(showContent ? 1 : 0)
Spacer()
// Unlock button
Button {
GlassButton(
icon: authManager.biometricIcon,
title: "Unlock with \(authManager.biometricName)"
) {
Task {
let success = await authManager.authenticate()
if !success {
showError = true
}
}
} label: {
HStack(spacing: 12) {
Image(systemName: authManager.biometricIcon)
.font(.title2)
Text("Unlock with \(authManager.biometricName)")
.fontWeight(.medium)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(Color.accentColor)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.disabled(authManager.isAuthenticating)
.padding(.horizontal, 40)
.opacity(showContent ? 1 : 0)
.offset(y: showContent ? 0 : 30)
.padding(.horizontal, 32)
// Passcode fallback hint
// Passcode hint
if authManager.canUseDevicePasscode {
Text("Or use your device passcode")
.font(.caption)
.foregroundStyle(.tertiary)
.font(.system(size: 12, weight: .regular, design: .rounded))
.foregroundColor(.white.opacity(0.35))
.padding(.top, 16)
.opacity(showContent ? 1 : 0)
}
Spacer()
.frame(height: 60)
.frame(height: 50)
}
.padding()
}
@@ -94,12 +472,30 @@ struct LockScreenView: View {
Text("Unable to verify your identity. Please try again.")
}
.onAppear {
// Auto-trigger authentication on appear
// Animate content in
withAnimation(.easeOut(duration: 0.8).delay(0.2)) {
showContent = true
}
// Auto-trigger authentication
if !authManager.isUnlocked && !authManager.isAuthenticating {
Task {
try? await Task.sleep(for: .milliseconds(800))
await authManager.authenticate()
}
}
}
}
}
// MARK: - Preview
#Preview("Lock Screen - Face ID") {
LockScreenView(authManager: BiometricAuthManager())
.preferredColorScheme(.dark)
}
#Preview("Lock Screen - Touch ID") {
LockScreenView(authManager: BiometricAuthManager())
.preferredColorScheme(.dark)
}