// // HapticFeedbackManager.swift // Reflect // // Haptic feedback patterns that match celebration animations. // import UIKit import CoreHaptics @MainActor final class HapticFeedbackManager { static let shared = HapticFeedbackManager() private var engine: CHHapticEngine? private init() { prepareEngine() } private func prepareEngine() { guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return } do { engine = try CHHapticEngine() engine?.resetHandler = { [weak self] in try? self?.engine?.start() } try engine?.start() } catch { engine = nil } } /// Play a haptic pattern that matches the given celebration animation type. func play(for animationType: CelebrationAnimationType) { guard CHHapticEngine.capabilitiesForHardware().supportsHaptics, let engine else { // Fall back to basic haptic on devices without CoreHaptics let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() return } do { let pattern = try hapticPattern(for: animationType) let player = try engine.makePlayer(with: pattern) try engine.start() try player.start(atTime: CHHapticTimeImmediate) } catch { // Fallback to simple impact let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() } } // MARK: - Pattern Definitions private func hapticPattern(for animationType: CelebrationAnimationType) throws -> CHHapticPattern { switch animationType { case .confettiCannon: return try confettiPattern() case .vortexCheckmark: return try vortexPattern() case .explosionReveal: return try explosionPattern() case .flipReveal: return try flipPattern() case .shatterReform: return try shatterPattern() case .pulseWave: return try pulseWavePattern() case .fireworks: return try fireworksPattern() case .morphBlob: return try morphPattern() case .zoomTunnel: return try tunnelPattern() case .gravityDrop: return try gravityPattern() } } // Confetti: poppy burst upward → checkmark spring → light scattered taps falling like confetti // Visual: 0.0 launch + glow | 0.3 checkmark spring | 0.5 confetti falling | 1.3 fade private func confettiPattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // Cannon pop — sharp, bright, like a party popper events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0.0 )) // Checkmark spring-in at 0.3s events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) ], relativeTime: 0.3 )) // Confetti pieces falling — light, playful, irregular taps with varying sharpness let confettiTimes: [Double] = [0.55, 0.65, 0.72, 0.82, 0.93, 1.05, 1.15, 1.25, 1.35, 1.45] let sharpnesses: [Float] = [0.9, 0.5, 0.8, 0.3, 0.7, 0.4, 0.6, 0.3, 0.5, 0.2] for (i, time) in confettiTimes.enumerated() { let intensity = Float(0.4 - Double(i) * 0.03) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: max(intensity, 0.1)), CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpnesses[i]) ], relativeTime: time )) } return try CHHapticPattern(events: events, parameters: []) } // Vortex: spirals spin 0–0.5s → ring snaps at 0.3s → checkmark pops 0.5s → settles 0.7s // Character: whirring buildup with accelerating ticks, then a tight satisfying snap private func vortexPattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // Spinning wind-up — accelerating ticks like a roulette wheel slowing in reverse let tickTimes: [Double] = [0.0, 0.07, 0.13, 0.18, 0.22, 0.25, 0.27, 0.29, 0.30] for (i, time) in tickTimes.enumerated() { let progress = Float(i) / Float(tickTimes.count) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15 + progress * 0.4), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2 + progress * 0.3) ], relativeTime: time )) } // Ring appears — soft thrum events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) ], relativeTime: 0.3, duration: 0.2 )) // Checkmark pops — the payoff snap events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6) ], relativeTime: 0.5 )) // Checkmark settles — soft bounce events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) ], relativeTime: 0.7 )) return try CHHapticPattern(events: events, parameters: []) } // Explosion: particles fly out 0–0.6s → glow 0.2–0.6s → checkmark drops in 0.3s → fades 1.4s // Character: massive singular BOOM then debris scattering away — the biggest hit of any animation private func explosionPattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // THE explosion — maximum everything, low sharpness for deep boom events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1) ], relativeTime: 0.0 )) // Shockwave rumble — deep, low, fading events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1) ], relativeTime: 0.02, duration: 0.35 )) // Debris particles hitting — scattered sharp ticks as pieces fly let debrisTimes: [Double] = [0.12, 0.18, 0.25, 0.32, 0.40] for (i, time) in debrisTimes.enumerated() { events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.5 - Double(i) * 0.08)), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.9) ], relativeTime: time )) } // Checkmark drops in — solid medium thud (not as big as the explosion) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) ], relativeTime: 0.55 )) let curve = CHHapticParameterCurve( parameterID: .hapticIntensityControl, controlPoints: [ CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 1.0), CHHapticParameterCurve.ControlPoint(relativeTime: 0.15, value: 0.6), CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.1) ], relativeTime: 0.0 ) return try CHHapticPattern(events: events, parameterCurves: [curve]) } // Flip: card flips at 0.2s → checkmark scales 0.4s → shimmer sweeps 0.5–1.3s // Character: clean, elegant — a crisp two-beat flip then a silky shimmer glide private func flipPattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // Card lifts off surface — gentle click events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7) ], relativeTime: 0.2 )) // Card slaps down — the satisfying flip landing events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) ], relativeTime: 0.4 )) // Checkmark bounce events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) ], relativeTime: 0.6 )) // Shimmer sweeping across — very light, high-sharpness continuous (like a sparkle) events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0.5, duration: 0.8 )) let shimmerCurve = CHHapticParameterCurve( parameterID: .hapticIntensityControl, controlPoints: [ CHHapticParameterCurve.ControlPoint(relativeTime: 0.5, value: 0.0), CHHapticParameterCurve.ControlPoint(relativeTime: 0.7, value: 1.0), CHHapticParameterCurve.ControlPoint(relativeTime: 1.3, value: 0.0) ], relativeTime: 0.0 ) return try CHHapticPattern(events: events, parameterCurves: [shimmerCurve]) } // Shatter: explode out 0–0.5s → pause → converge 0.6–1.1s → checkmark 1.1s → fade 1.8s // Character: glass breaking — sharp crack, scattered sharp hits, silence, then magnetic pull + click private func shatterPattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // Glass crack — extremely sharp and crisp events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0.0 )) // Shards flying — sharp, glassy ticks scattering outward let shardTimes: [Double] = [0.04, 0.08, 0.13, 0.19, 0.26, 0.35, 0.45] for (i, time) in shardTimes.enumerated() { events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.6 - Double(i) * 0.07)), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: time )) } // ---- silence / gap while pieces float (0.5–0.6s) ---- // Reform — pieces pulling back, low magnetic hum building events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1) ], relativeTime: 0.6, duration: 0.5 )) let reformCurve = CHHapticParameterCurve( parameterID: .hapticIntensityControl, controlPoints: [ CHHapticParameterCurve.ControlPoint(relativeTime: 0.6, value: 0.1), CHHapticParameterCurve.ControlPoint(relativeTime: 1.0, value: 0.6) ], relativeTime: 0.0 ) // Final snap together — satisfying click as pieces lock in events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6) ], relativeTime: 1.1 )) return try CHHapticPattern(events: events, parameterCurves: [reformCurve]) } // Pulse: waves at 0.0, 0.1, 0.2 → stamp checkmark at 0.2s with rotation → fade 1.2s // Character: rhythmic heartbeat pulses then a heavy rubber-stamp DOWN private func pulseWavePattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // Three wave pulses — each softer and more diffuse, like ripples spreading events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) ], relativeTime: 0.0 )) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15) ], relativeTime: 0.1 )) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.25), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1) ], relativeTime: 0.2 )) // STAMP DOWN — heavy, authoritative, like a rubber stamp slamming events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) ], relativeTime: 0.25 )) // Stamp reverb — brief low rumble from the impact events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.05) ], relativeTime: 0.27, duration: 0.2 )) return try CHHapticPattern(events: events, parameters: []) } // Fireworks: 4 bursts staggered at 0.0/0.15/0.30/0.45 → checkmark at 0.6s → fade 2.0s // Character: distinct pops at different heights/intensities, each with a quick sparkle trail private func fireworksPattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // Burst 1 — big opening burst events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7) ], relativeTime: 0.0 )) // Sparkle trail events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0.08 )) // Burst 2 — smaller, higher pitch events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.9) ], relativeTime: 0.15 )) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0.22 )) // Burst 3 — deep thump events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) ], relativeTime: 0.30 )) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.25), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8) ], relativeTime: 0.37 )) // Burst 4 — grand finale, biggest events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) ], relativeTime: 0.45 )) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.9) ], relativeTime: 0.52 )) // Checkmark — gentle after the show events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) ], relativeTime: 0.65 )) return try CHHapticPattern(events: events, parameters: []) } // Morph: blobs expand 0–0.4s → morph/shift 0.4–0.9s → converge 0.9–1.3s → checkmark 1.2s // Character: liquid, organic — continuous flowing hum that breathes and undulates private func morphPattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // Blob expand — soft, round, growing events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.05) ], relativeTime: 0.0, duration: 0.4 )) // Morph shift — blobs moving, undulating, slightly sharper events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1) ], relativeTime: 0.4, duration: 0.5 )) // Intensity curve for the morph — breathing/pulsing feel let morphCurve = CHHapticParameterCurve( parameterID: .hapticIntensityControl, controlPoints: [ CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.3), CHHapticParameterCurve.ControlPoint(relativeTime: 0.2, value: 0.5), CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.3), CHHapticParameterCurve.ControlPoint(relativeTime: 0.6, value: 0.6), CHHapticParameterCurve.ControlPoint(relativeTime: 0.8, value: 0.4), CHHapticParameterCurve.ControlPoint(relativeTime: 0.9, value: 0.8) ], relativeTime: 0.0 ) // Converge — blobs snap together, soft thud events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15) ], relativeTime: 0.9 )) // Checkmark — soft glow reveal, not sharp events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) ], relativeTime: 1.2 )) return try CHHapticPattern(events: events, parameterCurves: [morphCurve]) } // Tunnel: 6 rings zoom in (each 0.1s delay) → starburst rotates → checkmark 0.5s → fade 1.5s // Character: accelerating whoosh — rings pass you faster and faster, then BAM you arrive private func tunnelPattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // Rings passing — accelerating rhythm (gaps get shorter), each ring is a whoosh // Visual: rings at 0.0, 0.1, 0.2, 0.3, 0.4, 0.5 with scale animation let ringTimes: [Double] = [0.0, 0.08, 0.14, 0.19, 0.23, 0.26] for (i, time) in ringTimes.enumerated() { let progress = Float(i) / Float(ringTimes.count) // Each ring hit gets more intense and slightly sharper events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15 + progress * 0.5), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4 + progress * 0.3) ], relativeTime: time )) } // Warp speed — continuous buzzy rush as you fly through events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7) ], relativeTime: 0.28, duration: 0.2 )) let warpCurve = CHHapticParameterCurve( parameterID: .hapticIntensityControl, controlPoints: [ CHHapticParameterCurve.ControlPoint(relativeTime: 0.28, value: 0.4), CHHapticParameterCurve.ControlPoint(relativeTime: 0.38, value: 1.0), CHHapticParameterCurve.ControlPoint(relativeTime: 0.48, value: 0.8) ], relativeTime: 0.0 ) // Arrival — big thud as you land at the destination events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) ], relativeTime: 0.5 )) // Starburst shimmer — light rotating sparkle events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.9) ], relativeTime: 0.55, duration: 0.3 )) return try CHHapticPattern(events: events, parameterCurves: [warpCurve]) } // Gravity: falls 0–0.4s → impact 0.4s → bounce up 0.4–0.6 → down 0.6 → up 0.6–0.75 → down 0.75 → settle 0.85 // Character: weighty — falling acceleration, HEAVY impact, realistic diminishing bounces private func gravityPattern() throws -> CHHapticPattern { var events: [CHHapticEvent] = [] // Falling — accelerating continuous that builds events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) ], relativeTime: 0.0, duration: 0.38 )) let fallCurve = CHHapticParameterCurve( parameterID: .hapticIntensityControl, controlPoints: [ CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.05), CHHapticParameterCurve.ControlPoint(relativeTime: 0.2, value: 0.2), CHHapticParameterCurve.ControlPoint(relativeTime: 0.38, value: 0.9) ], relativeTime: 0.0 ) // FIRST IMPACT — heaviest, low and boomy (this is a heavy object hitting ground) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15) ], relativeTime: 0.4 )) // Dust/ring burst from impact events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) ], relativeTime: 0.41, duration: 0.12 )) // Bounce 1 — lands again, lighter events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.55), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) ], relativeTime: 0.6 )) // Bounce 2 — smaller events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.25) ], relativeTime: 0.75 )) // Final settle — tiny tap events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.12), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) ], relativeTime: 0.85 )) return try CHHapticPattern(events: events, parameterCurves: [fallCurve]) } }