Redesign haptic patterns per animation and play on selection

Rewrite all 10 celebration haptic patterns to match visual timing and
feel distinct: confetti gets playful falling taps, explosion gets a deep
boom, shatter gets glassy cracks, morph gets liquid breathing, etc.
Play the matching haptic when selecting a new vote animation in
customization (respects haptic feedback toggle).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-10 16:43:29 -05:00
parent 24a1a7b072
commit 45d83cff89
2 changed files with 459 additions and 328 deletions

View File

@@ -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 { private func confettiPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = [] var events: [CHHapticEvent] = []
// Initial burst // Cannon pop sharp, bright, like a party popper
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8) CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
], ],
relativeTime: 0.0 relativeTime: 0.0
)) ))
// Glow swell // Checkmark spring-in at 0.3s
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticContinuous, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
], ],
relativeTime: 0.1, relativeTime: 0.3
duration: 0.3
)) ))
// Scattered confetti taps falling // Confetti pieces falling light, playful, irregular taps with varying sharpness
let confettiTimes: [Double] = [0.5, 0.6, 0.65, 0.75, 0.85, 0.95, 1.1, 1.2] 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() { 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( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: max(intensity, 0.15)), CHHapticEventParameter(parameterID: .hapticIntensity, value: max(intensity, 0.1)),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpnesses[i])
], ],
relativeTime: time relativeTime: time
)) ))
} }
// Final checkmark thud return try CHHapticPattern(events: events, parameters: [])
}
// Vortex: spirals spin 00.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( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8), CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) 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: []) return try CHHapticPattern(events: events, parameters: [])
} }
// Vortex: spinning wind-up then a satisfying snap // Explosion: particles fly out 00.6s glow 0.20.6s checkmark drops in 0.3s fades 1.4s
private func vortexPattern() throws -> CHHapticPattern { // Character: massive singular BOOM then debris scattering away the biggest hit of any animation
private func explosionPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = [] var events: [CHHapticEvent] = []
// Spiral wind-up (continuous, rising intensity) // THE explosion maximum everything, low sharpness for deep boom
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
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ 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.51.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) 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 // 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( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), 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, parameterID: .hapticIntensityControl,
controlPoints: [ controlPoints: [
CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.2), CHHapticParameterCurve.ControlPoint(relativeTime: 0.5, value: 0.0),
CHHapticParameterCurve.ControlPoint(relativeTime: 0.4, value: 0.7), CHHapticParameterCurve.ControlPoint(relativeTime: 0.7, value: 1.0),
CHHapticParameterCurve.ControlPoint(relativeTime: 0.5, value: 1.0) CHHapticParameterCurve.ControlPoint(relativeTime: 1.3, value: 0.0)
], ],
relativeTime: 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 // Shatter: explode out 00.5s pause converge 0.61.1s checkmark 1.1s fade 1.8s
private func explosionPattern() throws -> CHHapticPattern { // Character: glass breaking sharp crack, scattered sharp hits, silence, then magnetic pull + click
private func shatterPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = [] var events: [CHHapticEvent] = []
// Big explosion impact // Glass crack extremely sharp and crisp
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
@@ -204,387 +312,406 @@ final class HapticFeedbackManager {
relativeTime: 0.0 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.50.6s) ----
// Reform pieces pulling back, low magnetic hum building
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticContinuous, eventType: .hapticContinuous,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8) CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
], ],
relativeTime: 0.05, relativeTime: 0.6,
duration: 0.4 duration: 0.5
)) ))
// Glow pulse let reformCurve = CHHapticParameterCurve(
events.append(CHHapticEvent( parameterID: .hapticIntensityControl,
eventType: .hapticContinuous, controlPoints: [
parameters: [ CHHapticParameterCurve.ControlPoint(relativeTime: 0.6, value: 0.1),
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), CHHapticParameterCurve.ControlPoint(relativeTime: 1.0, value: 0.6)
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)
], ],
relativeTime: 0.0 relativeTime: 0.0
)) )
// Mid-flip whoosh // Final snap together satisfying click as pieces lock in
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
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6) 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 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 { private func pulseWavePattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = [] var events: [CHHapticEvent] = []
// Three expanding wave pulses // Three wave pulses each softer and more diffuse, like ripples spreading
let waveTimes: [Double] = [0.0, 0.1, 0.2] events.append(CHHapticEvent(
for (i, time) in waveTimes.enumerated() { eventType: .hapticTransient,
events.append(CHHapticEvent( parameters: [
eventType: .hapticTransient, CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
parameters: [ CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.8 - Double(i) * 0.15)), ],
CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(0.6 - Double(i) * 0.1)) relativeTime: 0.0
], ))
relativeTime: time 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( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), 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( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3), 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 // Burst 3 deep thump
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
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8), 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) 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: []) return try CHHapticPattern(events: events, parameters: [])
} }
// Morph: flowing organic movement // Morph: blobs expand 00.4s morph/shift 0.40.9s converge 0.91.3s checkmark 1.2s
// Character: liquid, organic continuous flowing hum that breathes and undulates
private func morphPattern() throws -> CHHapticPattern { private func morphPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = [] var events: [CHHapticEvent] = []
// Expand outward // Blob expand soft, round, growing
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticContinuous, eventType: .hapticContinuous,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.05)
], ],
relativeTime: 0.0, relativeTime: 0.0,
duration: 0.4 duration: 0.4
)) ))
// Morph shift // Morph shift blobs moving, undulating, slightly sharper
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticContinuous, eventType: .hapticContinuous,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
], ],
relativeTime: 0.4, relativeTime: 0.4,
duration: 0.5 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( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.15)
], ],
relativeTime: 0.9 relativeTime: 0.9
)) ))
// Checkmark reveal // Checkmark soft glow reveal, not sharp
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
], ],
relativeTime: 1.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 { private func tunnelPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = [] var events: [CHHapticEvent] = []
// Rings rushing inward (6 quick taps, increasing intensity) // Rings passing accelerating rhythm (gaps get shorter), each ring is a whoosh
for i in 0..<6 { // Visual: rings at 0.0, 0.1, 0.2, 0.3, 0.4, 0.5 with scale animation
let time = Double(i) * 0.08 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( events.append(CHHapticEvent(
eventType: .hapticTransient, eventType: .hapticTransient,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.2 + Double(i) * 0.12)), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15 + progress * 0.5),
CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(0.3 + Double(i) * 0.1)) CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4 + progress * 0.3)
], ],
relativeTime: time relativeTime: time
)) ))
} }
// Warp speed continuous // Warp speed continuous buzzy rush as you fly through
events.append(CHHapticEvent( events.append(CHHapticEvent(
eventType: .hapticContinuous, eventType: .hapticContinuous,
parameters: [ parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6) CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
], ],
relativeTime: 0.3, relativeTime: 0.28,
duration: 0.2 duration: 0.2
)) ))
// Arrival impact let warpCurve = CHHapticParameterCurve(
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(
parameterID: .hapticIntensityControl, parameterID: .hapticIntensityControl,
controlPoints: [ controlPoints: [
CHHapticParameterCurve.ControlPoint(relativeTime: 0.0, value: 0.1), CHHapticParameterCurve.ControlPoint(relativeTime: 0.28, value: 0.4),
CHHapticParameterCurve.ControlPoint(relativeTime: 0.35, value: 0.8) CHHapticParameterCurve.ControlPoint(relativeTime: 0.38, value: 1.0),
CHHapticParameterCurve.ControlPoint(relativeTime: 0.48, value: 0.8)
], ],
relativeTime: 0.0 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 00.4s impact 0.4s bounce up 0.40.6 down 0.6 up 0.60.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])
} }
} }

View File

@@ -495,6 +495,7 @@ struct CelebrationAnimationPickerCompact: View {
@AppStorage(UserDefaultsStore.Keys.celebrationAnimation.rawValue, store: GroupUserDefaults.groupDefaults) private var celebrationAnimationIndex: Int = 0 @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.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.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 @Environment(\.colorScheme) private var colorScheme
// Preview state // Preview state
@@ -586,6 +587,9 @@ struct CelebrationAnimationPickerCompact: View {
Task { @MainActor in Task { @MainActor in
try? await Task.sleep(for: .seconds(0.5)) try? await Task.sleep(for: .seconds(0.5))
guard previewAnimation == animation else { return } guard previewAnimation == animation else { return }
if hapticFeedbackEnabled {
HapticFeedbackManager.shared.play(for: animation)
}
withAnimation(.easeInOut(duration: 0.3)) { withAnimation(.easeInOut(duration: 0.3)) {
showPreviewCelebration = true showPreviewCelebration = true
} }