// // DebugAnimationSettingsView.swift // Feels // // Debug-only view for experimenting with vote celebration animations. // import SwiftUI // MARK: - Main Debug View struct DebugAnimationSettingsView: View { @Environment(\.dismiss) private var dismiss @Environment(\.colorScheme) private var colorScheme @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system private var textColor: Color { theme.currentTheme.labelColor } @State private var selectedAnimation: CelebrationAnimationType = .vortexCheckmark @State private var isAnimating = false private var isDark: Bool { colorScheme == .dark } var body: some View { VStack(spacing: 0) { // Lab Header labHeader .padding(.top, 8) // Animation Picker - Horizontal Scroll Cards animationPicker .padding(.top, 16) // Preview Area previewArea .padding(.top, 20) .padding(.horizontal, 16) Spacer() } .background( ZStack { theme.currentTheme.bg // Subtle grid pattern for "lab" feel GridPatternView() .opacity(isDark ? 0.03 : 0.02) } .ignoresSafeArea() ) .navigationTitle("") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Done") { dismiss() } } } } // MARK: - Lab Header private var labHeader: some View { VStack(spacing: 6) { HStack(spacing: 8) { Image(systemName: "flask.fill") .font(.title2) .foregroundStyle( LinearGradient( colors: [.purple, .pink], startPoint: .topLeading, endPoint: .bottomTrailing ) ) Text("Animation Lab") .font(.system(size: 24, weight: .bold, design: .rounded)) .foregroundColor(textColor) } Text("Experiment with vote celebrations") .font(.system(size: 14, weight: .medium, design: .rounded)) .foregroundColor(.secondary) } } // MARK: - Animation Picker private var animationPicker: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(CelebrationAnimationType.allCases) { type in AnimationCard( type: type, isSelected: selectedAnimation == type, isDark: isDark ) { withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { selectedAnimation = type } } } } .padding(.horizontal, 16) .padding(.vertical, 8) } } // MARK: - Preview Area private var previewArea: some View { VStack(spacing: 12) { // Current selection indicator HStack(spacing: 6) { Circle() .fill(selectedAnimation.accentColor) .frame(width: 8, height: 8) Text(selectedAnimation.rawValue) .font(.system(size: 13, weight: .semibold, design: .monospaced)) .foregroundColor(selectedAnimation.accentColor) Text("—") .foregroundColor(.secondary.opacity(0.5)) Text(selectedAnimation.description) .font(.system(size: 13, weight: .medium, design: .rounded)) .foregroundColor(.secondary) } // The animated voting view AnimatedVotingView( animationType: selectedAnimation, isAnimating: $isAnimating ) .frame(height: 280) .background( RoundedRectangle(cornerRadius: 24) .fill(isDark ? Color(.systemGray6) : .white) .shadow(color: .black.opacity(isDark ? 0.3 : 0.08), radius: 20, x: 0, y: 8) ) .overlay( RoundedRectangle(cornerRadius: 24) .stroke(selectedAnimation.accentColor.opacity(0.3), lineWidth: 1) ) .clipShape(RoundedRectangle(cornerRadius: 24)) } } } // MARK: - Grid Pattern Background struct GridPatternView: View { var body: some View { Canvas { context, size in let gridSize: CGFloat = 20 for x in stride(from: 0, to: size.width, by: gridSize) { for y in stride(from: 0, to: size.height, by: gridSize) { let rect = CGRect(x: x, y: y, width: 1, height: 1) context.fill(Path(ellipseIn: rect), with: .color(.primary)) } } } } } // MARK: - Animation Card struct AnimationCard: View { let type: CelebrationAnimationType let isSelected: Bool let isDark: Bool let onTap: () -> Void @State private var isPressed = false var body: some View { Button(action: onTap) { VStack(spacing: 8) { // Icon ZStack { Circle() .fill( isSelected ? type.accentColor.opacity(0.2) : (isDark ? Color(.systemGray5) : Color(.systemGray6)) ) .frame(width: 44, height: 44) Image(systemName: type.icon) .font(.system(size: 20, weight: .medium)) .foregroundColor(isSelected ? type.accentColor : .secondary) } // Name Text(type.rawValue) .font(.system(size: 11, weight: .semibold, design: .rounded)) .foregroundColor(isSelected ? type.accentColor : .secondary) } .frame(width: 72, height: 80) .background( RoundedRectangle(cornerRadius: 16) .fill(isDark ? Color(.systemGray6) : .white) .shadow( color: isSelected ? type.accentColor.opacity(0.3) : .black.opacity(0.05), radius: isSelected ? 8 : 4, x: 0, y: isSelected ? 4 : 2 ) ) .overlay( RoundedRectangle(cornerRadius: 16) .stroke( isSelected ? type.accentColor : Color.clear, lineWidth: 2 ) ) .scaleEffect(isPressed ? 0.95 : (isSelected ? 1.02 : 1.0)) } .buttonStyle(PlainButtonStyle()) .onLongPressGesture(minimumDuration: .infinity, pressing: { pressing in withAnimation(.easeInOut(duration: 0.15)) { isPressed = pressing } }, perform: {}) } } // MARK: - Animated Voting View struct AnimatedVotingView: View { let animationType: CelebrationAnimationType @Binding var isAnimating: Bool @State private var selectedMood: Mood = .great // Animation state @State private var animationScale: CGFloat = 1 @State private var animationRotation: Angle = .zero @State private var animationOffset: CGSize = .zero var body: some View { ZStack { // The voting content (gets animated) DebugVotingContentView(selectedMood: $selectedMood) { triggerAnimation() } .opacity(isAnimating ? 0 : 1) .scaleEffect(animationScale) .rotation3DEffect(animationRotation, axis: (x: 0, y: 1, z: 0)) .offset(animationOffset) // Animation overlay if isAnimating { CelebrationOverlayView( animationType: animationType, mood: selectedMood ) { // Reset handled by triggerAnimation } } } .animation(.spring(response: 0.5, dampingFraction: 0.7), value: isAnimating) } private func triggerAnimation() { guard !isAnimating else { return } let impact = UIImpactFeedbackGenerator(style: .medium) impact.impactOccurred() // Set animation properties based on type withAnimation(.easeInOut(duration: 0.4)) { isAnimating = true switch animationType { case .vortexCheckmark: animationScale = 0.01 case .explosionReveal: animationScale = 2.5 case .flipReveal: animationRotation = .degrees(90) case .shatterReform: break case .pulseWave: animationScale = 0.9 case .fireworks: animationScale = 0.8 case .confettiCannon: animationScale = 0.95 case .morphBlob: animationScale = 0.5 case .zoomTunnel: animationScale = 0.01 case .gravityDrop: animationOffset = CGSize(width: 0, height: 400) } } DispatchQueue.main.asyncAfter(deadline: .now() + animationType.duration) { withAnimation(.easeOut(duration: 0.3)) { isAnimating = false animationScale = 1 animationRotation = .zero animationOffset = .zero } } } } // MARK: - Debug Voting Content View struct DebugVotingContentView: View { @Binding var selectedMood: Mood let onVote: () -> Void var body: some View { VStack(spacing: 20) { Text("How are you feeling?") .font(.system(size: 18, weight: .semibold, design: .rounded)) .foregroundColor(.primary) HStack(spacing: 12) { ForEach(Mood.allValues, id: \.self) { mood in Button { selectedMood = mood onVote() } label: { mood.icon .resizable() .scaledToFit() .frame(width: 40, height: 40) .padding(8) .background( Circle() .fill(mood.color.opacity(0.15)) ) } } } Text("Tap to vote") .font(.system(size: 12, weight: .medium, design: .rounded)) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } // MARK: - Preview #Preview { NavigationStack { DebugAnimationSettingsView() } }