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,219 +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
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
relativeTime: 0.1,
duration: 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]
for (i, time) in confettiTimes.enumerated() {
let intensity = Float(0.6 - Double(i) * 0.05)
// Checkmark spring-in at 0.3s
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: max(intensity, 0.15)),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
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
))
}
// 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.8),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15 + progress * 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2 + progress * 0.3)
],
relativeTime: 1.4
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: [])
}
// Vortex: spinning wind-up then a satisfying snap
private func vortexPattern() throws -> CHHapticPattern {
// Explosion: particles fly out 00.6s glow 0.20.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.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)
],
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)
],
relativeTime: 0.9
))
let curve = 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)
],
relativeTime: 0.0
)
return try CHHapticPattern(events: events, parameterCurves: [curve])
}
// Explosion: big bang then particles scatter
private func explosionPattern() throws -> CHHapticPattern {
var events: [CHHapticEvent] = []
// Big explosion impact
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0.0
))
// Debris rumble
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
],
relativeTime: 0.05,
duration: 0.4
))
// 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)
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
],
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
))
// Mid-flip whoosh
// Shimmer sweeping across very light, high-sharpness continuous (like a sparkle)
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(
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)
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.15),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: 0.5,
duration: 0.4
duration: 0.8
))
return try CHHapticPattern(events: events, parameters: [])
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: crack apart then reform
// Shatter: explode out 00.5s pause converge 0.61.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] = []
// Shatter crack
// Glass crack extremely sharp and crisp
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
@@ -302,289 +312,406 @@ final class HapticFeedbackManager {
relativeTime: 0.0
))
// Flying shards
let shardTimes: [Double] = [0.05, 0.1, 0.15, 0.2, 0.3]
// 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.7 - Double(i) * 0.1)),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.8)
CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(0.6 - Double(i) * 0.07)),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0)
],
relativeTime: time
))
}
// Reform convergence
// ---- silence / gap while pieces float (0.50.6s) ----
// Reform pieces pulling back, low magnetic hum building
events.append(CHHapticEvent(
eventType: .hapticContinuous,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.4),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3)
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.1)
],
relativeTime: 0.6,
duration: 0.4
duration: 0.5
))
// Checkmark snap
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 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: 1.1
))
relativeTime: 0.0
)
return try CHHapticPattern(events: events, parameters: [])
}
// Pulse Wave: expanding rings
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
))
}
// Stamp down (checkmark)
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.7)
],
relativeTime: 0.3
))
// Stamp settle
events.append(CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.3),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4)
],
relativeTime: 0.45
))
return try CHHapticPattern(events: events, parameters: [])
}
// 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
// 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.8)
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.6)
],
relativeTime: time + 0.1
relativeTime: 1.1
))
return try CHHapticPattern(events: events, parameterCurves: [reformCurve])
}
// Final checkmark
// 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.8),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2)
],
relativeTime: 0.7
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: [])
}
// Morph: flowing organic movement
// 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 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 {
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 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.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
}