diff --git a/Shared/Services/HapticFeedbackManager.swift b/Shared/Services/HapticFeedbackManager.swift index d1cdd54..47e7fb4 100644 --- a/Shared/Services/HapticFeedbackManager.swift +++ b/Shared/Services/HapticFeedbackManager.swift @@ -80,121 +80,229 @@ final class HapticFeedbackManager { } } - // Confetti: burst upward, then scattered taps falling down + // 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] = [] - // Initial burst + // Cannon pop — sharp, bright, like a party popper events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0.0 )) - // Glow swell + // Checkmark spring-in at 0.3s events.append(CHHapticEvent( - eventType: .hapticContinuous, + eventType: .hapticTransient, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) ], - relativeTime: 0.1, - duration: 0.3 + relativeTime: 0.3 )) - // Scattered confetti taps falling - let confettiTimes: [Double] = [0.5, 0.6, 0.65, 0.75, 0.85, 0.95, 1.1, 1.2] + // 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.6 - Double(i) * 0.05) + let intensity = Float(0.4 - Double(i) * 0.03) events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: max(intensity, 0.15)), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) + CHHapticEventParameter(parameterID: .hapticIntensity, value: max(intensity, 0.1)), + CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpnesses[i]) ], relativeTime: time )) } - // Final checkmark thud + 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: 0.8), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6) ], - relativeTime: 1.4 + 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: []) } - // Vortex: spinning wind-up then a satisfying snap - private func vortexPattern() throws -> CHHapticPattern { + // 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] = [] - // Spiral wind-up (continuous, rising intensity) - events.append(CHHapticEvent( - eventType: .hapticContinuous, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) - ], - relativeTime: 0.0, - duration: 0.5 - )) - - // Ring snap + // THE explosion — maximum everything, low sharpness for deep boom events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9), + 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.5 + 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: 1.0), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6) - ], - relativeTime: 0.7 - )) - - // Settle events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) ], - relativeTime: 0.9 + relativeTime: 0.6 )) - let curve = CHHapticParameterCurve( + // 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.0, value: 0.2), - CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.7), - CHHapticParameterCurve.ControlPoint(relativeTime: 0.5, value: 1.0) + 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: [curve]) + return try CHHapticPattern(events: events, parameterCurves: [shimmerCurve]) } - // Explosion: big bang then particles scatter - private func explosionPattern() throws -> CHHapticPattern { + // 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] = [] - // Big explosion impact + // Glass crack — extremely sharp and crisp events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ @@ -204,387 +312,406 @@ final class HapticFeedbackManager { relativeTime: 0.0 )) - // Debris rumble + // 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.6), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1) ], - relativeTime: 0.05, - duration: 0.4 + relativeTime: 0.6, + duration: 0.5 )) - // Glow pulse - events.append(CHHapticEvent( - eventType: .hapticContinuous, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) - ], - relativeTime: 0.3, - duration: 0.3 - )) - - // Checkmark landing - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) - ], - relativeTime: 0.6 - )) - - return try CHHapticPattern(events: events, parameters: []) - } - - // Flip: a quick flip-over thud - private func flipPattern() throws -> CHHapticPattern { - var events: [CHHapticEvent] = [] - - // Card lift - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) + 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 - )) + ) - // Mid-flip whoosh - events.append(CHHapticEvent( - eventType: .hapticContinuous, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) - ], - relativeTime: 0.2, - duration: 0.2 - )) - - // Card lands + // 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: 0.4 - )) - - // Shimmer glide - events.append(CHHapticEvent( - eventType: .hapticContinuous, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7) - ], - relativeTime: 0.5, - duration: 0.4 - )) - - return try CHHapticPattern(events: events, parameters: []) - } - - // Shatter: crack apart then reform - private func shatterPattern() throws -> CHHapticPattern { - var events: [CHHapticEvent] = [] - - // Shatter crack - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) - ], - relativeTime: 0.0 - )) - - // Flying shards - let shardTimes: [Double] = [0.05, 0.1, 0.15, 0.2, 0.3] - for (i, time) in shardTimes.enumerated() { - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.7 - Double(i) * 0.1)), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8) - ], - relativeTime: time - )) - } - - // Reform convergence - events.append(CHHapticEvent( - eventType: .hapticContinuous, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) - ], - relativeTime: 0.6, - duration: 0.4 - )) - - // Checkmark snap - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) - ], relativeTime: 1.1 )) - return try CHHapticPattern(events: events, parameters: []) + return try CHHapticPattern(events: events, parameterCurves: [reformCurve]) } - // Pulse Wave: expanding rings + // 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 expanding wave pulses - let waveTimes: [Double] = [0.0, 0.1, 0.2] - for (i, time) in waveTimes.enumerated() { - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.8 - Double(i) * 0.15)), - CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(0.6 - Double(i) * 0.1)) - ], - relativeTime: time - )) - } + // 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 (checkmark) + // 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.7) + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) ], - relativeTime: 0.3 + relativeTime: 0.25 )) - // Stamp settle + // 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: 0.4) + CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], - relativeTime: 0.45 + relativeTime: 0.08 )) - return try CHHapticPattern(events: events, parameters: []) - } + // 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 + )) - // Fireworks: multiple bursts at staggered intervals - private func fireworksPattern() throws -> CHHapticPattern { - var events: [CHHapticEvent] = [] - - // 4 firework bursts, each staggered - let burstTimes: [Double] = [0.0, 0.15, 0.3, 0.45] - for time in burstTimes { - // Launch whoosh - events.append(CHHapticEvent( - eventType: .hapticContinuous, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) - ], - relativeTime: time, - duration: 0.1 - )) - - // Burst pop - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8) - ], - relativeTime: time + 0.1 - )) - } - - // Final checkmark + // 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.7 + 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: flowing organic movement + // 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] = [] - // Expand outward + // Blob expand — soft, round, growing events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.05) ], relativeTime: 0.0, duration: 0.4 )) - // Morph shift + // Morph shift — blobs moving, undulating, slightly sharper events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1) ], relativeTime: 0.4, duration: 0.5 )) - // Converge + // 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.6), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15) ], relativeTime: 0.9 )) - // Checkmark reveal + // Checkmark — soft glow reveal, not sharp events.append(CHHapticEvent( eventType: .hapticTransient, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) ], relativeTime: 1.2 )) - return try CHHapticPattern(events: events, parameters: []) + return try CHHapticPattern(events: events, parameterCurves: [morphCurve]) } - // Tunnel: rushing zoom then arrival + // 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 rushing inward (6 quick taps, increasing intensity) - for i in 0..<6 { - let time = Double(i) * 0.08 + // 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: Float(0.2 + Double(i) * 0.12)), - CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(0.3 + Double(i) * 0.1)) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15 + progress * 0.5), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4 + progress * 0.3) ], relativeTime: time )) } - // Warp speed continuous + // Warp speed — continuous buzzy rush as you fly through events.append(CHHapticEvent( eventType: .hapticContinuous, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6) + CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), + CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7) ], - relativeTime: 0.3, + relativeTime: 0.28, duration: 0.2 )) - // Arrival impact - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7) - ], - relativeTime: 0.5 - )) - - // Settle bounce - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) - ], - relativeTime: 0.7 - )) - - return try CHHapticPattern(events: events, parameters: []) - } - - // Gravity: fall, bounce, bounce, settle - private func gravityPattern() throws -> CHHapticPattern { - var events: [CHHapticEvent] = [] - - // Falling whoosh - events.append(CHHapticEvent( - eventType: .hapticContinuous, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) - ], - relativeTime: 0.0, - duration: 0.35 - )) - - // First impact (heaviest) - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8) - ], - relativeTime: 0.4 - )) - - // First bounce up & down - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6) - ], - relativeTime: 0.6 - )) - - // Second bounce (smaller) - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.35), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) - ], - relativeTime: 0.75 - )) - - // Final settle - events.append(CHHapticEvent( - eventType: .hapticTransient, - parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) - ], - relativeTime: 0.85 - )) - - let curve = CHHapticParameterCurve( + let warpCurve = CHHapticParameterCurve( parameterID: .hapticIntensityControl, controlPoints: [ - CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.1), - CHHapticParameterCurve.ControlPoint(relativeTime: 0.35, value: 0.8) + 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 ) - return try CHHapticPattern(events: events, parameterCurves: [curve]) + // 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]) } } diff --git a/Shared/Views/CustomizeView/CustomizeView.swift b/Shared/Views/CustomizeView/CustomizeView.swift index 6a153a8..c93fbb6 100644 --- a/Shared/Views/CustomizeView/CustomizeView.swift +++ b/Shared/Views/CustomizeView/CustomizeView.swift @@ -495,6 +495,7 @@ struct CelebrationAnimationPickerCompact: View { @AppStorage(UserDefaultsStore.Keys.celebrationAnimation.rawValue, store: GroupUserDefaults.groupDefaults) private var celebrationAnimationIndex: Int = 0 @AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system + @AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true @Environment(\.colorScheme) private var colorScheme // Preview state @@ -586,6 +587,9 @@ struct CelebrationAnimationPickerCompact: View { Task { @MainActor in try? await Task.sleep(for: .seconds(0.5)) guard previewAnimation == animation else { return } + if hapticFeedbackEnabled { + HapticFeedbackManager.shared.play(for: animation) + } withAnimation(.easeInOut(duration: 0.3)) { showPreviewCelebration = true }