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