Files
Reflect/Shared/Views/SettingsView/DebugAnimationSettingsView.swift
Trey t 0442eab1f8 Rebrand entire project from Feels to Reflect
Complete rename across all bundle IDs, App Groups, CloudKit containers,
StoreKit product IDs, data store filenames, URL schemes, logger subsystems,
Swift identifiers, user-facing strings (7 languages), file names, directory
names, Xcode project, schemes, assets, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 11:47:16 -06:00

357 lines
11 KiB
Swift

//
// DebugAnimationSettingsView.swift
// Reflect
//
// 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()
}
}