@renegades/react-native-tickle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +352 -0
  3. package/Tickle.podspec +29 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +126 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/tickle/Tickle.kt +71 -0
  10. package/android/src/main/java/com/margelo/nitro/tickle/TicklePackage.kt +22 -0
  11. package/ios/Tickle.swift +185 -0
  12. package/ios/TickleUtils.swift +404 -0
  13. package/lib/module/Tickle.nitro.js +4 -0
  14. package/lib/module/Tickle.nitro.js.map +1 -0
  15. package/lib/module/index.js +254 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/typescript/package.json +1 -0
  19. package/lib/typescript/src/Tickle.nitro.d.ts +63 -0
  20. package/lib/typescript/src/Tickle.nitro.d.ts.map +1 -0
  21. package/lib/typescript/src/index.d.ts +148 -0
  22. package/lib/typescript/src/index.d.ts.map +1 -0
  23. package/nitro.json +17 -0
  24. package/nitrogen/generated/android/c++/JHapticCurve.hpp +87 -0
  25. package/nitrogen/generated/android/c++/JHapticCurveControlPoint.hpp +61 -0
  26. package/nitrogen/generated/android/c++/JHapticEvent.hpp +94 -0
  27. package/nitrogen/generated/android/c++/JHapticEventParameter.hpp +62 -0
  28. package/nitrogen/generated/android/c++/JHapticEventType.hpp +58 -0
  29. package/nitrogen/generated/android/c++/JHapticImpactStyle.hpp +67 -0
  30. package/nitrogen/generated/android/c++/JHapticNotificationType.hpp +61 -0
  31. package/nitrogen/generated/android/c++/JHapticParameterType.hpp +58 -0
  32. package/nitrogen/generated/android/c++/JHybridTickleSpec.cpp +162 -0
  33. package/nitrogen/generated/android/c++/JHybridTickleSpec.hpp +79 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticCurve.kt +44 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticCurveControlPoint.kt +41 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticEvent.kt +47 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticEventParameter.kt +41 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticEventType.kt +23 -0
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticImpactStyle.kt +26 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticNotificationType.kt +24 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticParameterType.kt +23 -0
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HybridTickleSpec.kt +109 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/tickleOnLoad.kt +35 -0
  44. package/nitrogen/generated/android/tickle+autolinking.cmake +81 -0
  45. package/nitrogen/generated/android/tickle+autolinking.gradle +27 -0
  46. package/nitrogen/generated/android/tickleOnLoad.cpp +44 -0
  47. package/nitrogen/generated/android/tickleOnLoad.hpp +25 -0
  48. package/nitrogen/generated/ios/Tickle+autolinking.rb +60 -0
  49. package/nitrogen/generated/ios/Tickle-Swift-Cxx-Bridge.cpp +33 -0
  50. package/nitrogen/generated/ios/Tickle-Swift-Cxx-Bridge.hpp +139 -0
  51. package/nitrogen/generated/ios/Tickle-Swift-Cxx-Umbrella.hpp +70 -0
  52. package/nitrogen/generated/ios/TickleAutolinking.mm +33 -0
  53. package/nitrogen/generated/ios/TickleAutolinking.swift +25 -0
  54. package/nitrogen/generated/ios/c++/HybridTickleSpecSwift.cpp +11 -0
  55. package/nitrogen/generated/ios/c++/HybridTickleSpecSwift.hpp +185 -0
  56. package/nitrogen/generated/ios/swift/HapticCurve.swift +46 -0
  57. package/nitrogen/generated/ios/swift/HapticCurveControlPoint.swift +35 -0
  58. package/nitrogen/generated/ios/swift/HapticEvent.swift +57 -0
  59. package/nitrogen/generated/ios/swift/HapticEventParameter.swift +35 -0
  60. package/nitrogen/generated/ios/swift/HapticEventType.swift +40 -0
  61. package/nitrogen/generated/ios/swift/HapticImpactStyle.swift +52 -0
  62. package/nitrogen/generated/ios/swift/HapticNotificationType.swift +44 -0
  63. package/nitrogen/generated/ios/swift/HapticParameterType.swift +40 -0
  64. package/nitrogen/generated/ios/swift/HybridTickleSpec.swift +69 -0
  65. package/nitrogen/generated/ios/swift/HybridTickleSpec_cxx.swift +282 -0
  66. package/nitrogen/generated/shared/c++/HapticCurve.hpp +96 -0
  67. package/nitrogen/generated/shared/c++/HapticCurveControlPoint.hpp +87 -0
  68. package/nitrogen/generated/shared/c++/HapticEvent.hpp +101 -0
  69. package/nitrogen/generated/shared/c++/HapticEventParameter.hpp +88 -0
  70. package/nitrogen/generated/shared/c++/HapticEventType.hpp +76 -0
  71. package/nitrogen/generated/shared/c++/HapticImpactStyle.hpp +88 -0
  72. package/nitrogen/generated/shared/c++/HapticNotificationType.hpp +80 -0
  73. package/nitrogen/generated/shared/c++/HapticParameterType.hpp +76 -0
  74. package/nitrogen/generated/shared/c++/HybridTickleSpec.cpp +34 -0
  75. package/nitrogen/generated/shared/c++/HybridTickleSpec.hpp +87 -0
  76. package/package.json +179 -0
  77. package/react-native.config.js +8 -0
  78. package/src/Tickle.nitro.ts +84 -0
  79. package/src/index.tsx +306 -0
@@ -0,0 +1,185 @@
1
+ import CoreHaptics
2
+ import UIKit
3
+
4
+ struct AnyHapticAnimationState: HapticAnimationState {
5
+ var hapticEvents: [CHHapticEvent]
6
+ var hapticCurves: [CHHapticParameterCurve]
7
+
8
+ init(hapticEvents: [CHHapticEvent], hapticCurves: [CHHapticParameterCurve]) {
9
+ self.hapticEvents = hapticEvents
10
+ self.hapticCurves = hapticCurves
11
+ }
12
+ }
13
+
14
+
15
+ class Tickle: HybridTickleSpec {
16
+ func stopAllHaptics() throws {
17
+ haptics.stopAllHapticPlayers()
18
+ }
19
+
20
+ func initializeEngine() throws {
21
+ haptics.createAndStartHapticEngine()
22
+ }
23
+
24
+ func destroyEngine() throws {
25
+ haptics.destroyEngine()
26
+ }
27
+
28
+ // MARK: - Continuous Player Methods
29
+
30
+ func createContinuousPlayer(playerId: String, initialIntensity: Double, initialSharpness: Double) throws {
31
+ haptics.createContinuousPlayer(playerId: playerId, initialIntensity: Float(initialIntensity), initialSharpness: Float(initialSharpness))
32
+ }
33
+
34
+ func startContinuousPlayer(playerId: String) throws {
35
+ haptics.startContinuousPlayer(playerId: playerId)
36
+ }
37
+
38
+ func updateContinuousPlayer(playerId: String, intensityControl: Double, sharpnessControl: Double) throws {
39
+ haptics.updateContinuousPlayer(playerId: playerId, intensityControl: Float(intensityControl), sharpnessControl: Float(sharpnessControl))
40
+ }
41
+
42
+ func stopContinuousPlayer(playerId: String) throws {
43
+ haptics.stopContinuousPlayer(playerId: playerId)
44
+ }
45
+
46
+ func destroyContinuousPlayer(playerId: String) throws {
47
+ haptics.destroyContinuousPlayer(playerId: playerId)
48
+ }
49
+
50
+ // MARK: - Global Haptics Enable/Disable
51
+
52
+ func setHapticsEnabled(enabled: Bool) throws {
53
+ haptics.hapticsEnabled = enabled
54
+ }
55
+
56
+ func getHapticsEnabled() throws -> Bool {
57
+ return haptics.hapticsEnabled
58
+ }
59
+
60
+
61
+ func startHaptic(events: [HapticEvent], curves: [HapticCurve]) throws {
62
+ let hapticEvents = events.map { event -> CHHapticEvent in
63
+ let eventType: CHHapticEvent.EventType = (event.type == .continuous) ? .hapticContinuous : .hapticTransient
64
+ let parameters = event.parameters.map { parameter -> CHHapticEventParameter in
65
+ let parameterID: CHHapticEvent.ParameterID = (parameter.type == .intensity) ? .hapticIntensity : .hapticSharpness
66
+ return CHHapticEventParameter(parameterID: parameterID, value: Float(parameter.value))
67
+ }
68
+ return CHHapticEvent(
69
+ eventType: eventType,
70
+ parameters: parameters,
71
+ relativeTime: event.relativeTime / 1000.0,
72
+ duration: event.type == .continuous ? (event.duration ?? 1000.0) / 1000.0 : 0
73
+ )
74
+ }
75
+
76
+ var hapticCurves: [CHHapticParameterCurve] = []
77
+ for curve in curves {
78
+ let parameterID: CHHapticDynamicParameter.ID = (curve.type == .intensity) ? .hapticIntensityControl : .hapticSharpnessControl
79
+
80
+ var controlPoints: [CHHapticParameterCurve.ControlPoint] = []
81
+ for controlPoint in curve.controlPoints {
82
+ let point = CHHapticParameterCurve.ControlPoint(
83
+ relativeTime: controlPoint.relativeTime / 1000.0,
84
+ value: Float(controlPoint.value)
85
+ )
86
+ controlPoints.append(point)
87
+ }
88
+
89
+ // Find the matching continuous event to get its duration.
90
+ // The curve's relativeTime should match a continuous event's relativeTime.
91
+ var matchingEvent: HapticEvent? = nil
92
+ for event in events {
93
+ let isContinuous = event.type == .continuous
94
+ let diff = event.relativeTime - curve.relativeTime
95
+ let timeDiff = diff < 0 ? -diff : diff
96
+ if isContinuous && timeDiff < 1 {
97
+ matchingEvent = event
98
+ break
99
+ }
100
+ }
101
+
102
+ // Add a reset control point at the end of the continuous event.
103
+ // This ensures hapticIntensityControl/hapticSharpnessControl return to 1.0 (neutral)
104
+ // so subsequent events (like transients) aren't affected by this curve's final value.
105
+ if let event = matchingEvent, let duration = event.duration {
106
+ let resetTime = duration / 1000.0
107
+ // Only add if it's after the last control point
108
+ if let lastPoint = controlPoints.last, resetTime > lastPoint.relativeTime {
109
+ controlPoints.append(CHHapticParameterCurve.ControlPoint(relativeTime: resetTime, value: 1.0))
110
+ }
111
+ }
112
+
113
+ let hapticCurve = CHHapticParameterCurve(
114
+ parameterID: parameterID,
115
+ controlPoints: controlPoints,
116
+ relativeTime: curve.relativeTime / 1000.0
117
+ )
118
+ hapticCurves.append(hapticCurve)
119
+ }
120
+
121
+ let state = AnyHapticAnimationState(hapticEvents: hapticEvents, hapticCurves: hapticCurves)
122
+ haptics.createHapticPlayers(for: [state])
123
+ haptics.startHapticPlayer(for: state)
124
+ }
125
+
126
+ // MARK: - System Haptics (Predefined OS-level feedback)
127
+
128
+ func triggerImpact(style: HapticImpactStyle) throws {
129
+ guard haptics.hapticsEnabled else { return }
130
+
131
+ let feedbackStyle = style.toUIFeedbackStyle()
132
+ let generator = UIImpactFeedbackGenerator(style: feedbackStyle)
133
+ generator.prepare()
134
+ generator.impactOccurred()
135
+ }
136
+
137
+ func triggerNotification(type: HapticNotificationType) throws {
138
+ guard haptics.hapticsEnabled else { return }
139
+
140
+ let feedbackType = type.toUIFeedbackType()
141
+ let generator = UINotificationFeedbackGenerator()
142
+ generator.prepare()
143
+ generator.notificationOccurred(feedbackType)
144
+ }
145
+
146
+ func triggerSelection() throws {
147
+ guard haptics.hapticsEnabled else { return }
148
+
149
+ let generator = UISelectionFeedbackGenerator()
150
+ generator.prepare()
151
+ generator.selectionChanged()
152
+ }
153
+ }
154
+
155
+ // MARK: - Type Conversions
156
+
157
+ extension HapticImpactStyle {
158
+ func toUIFeedbackStyle() -> UIImpactFeedbackGenerator.FeedbackStyle {
159
+ switch self {
160
+ case .rigid:
161
+ return .rigid
162
+ case .heavy:
163
+ return .heavy
164
+ case .medium:
165
+ return .medium
166
+ case .light:
167
+ return .light
168
+ case .soft:
169
+ return .soft
170
+ }
171
+ }
172
+ }
173
+
174
+ extension HapticNotificationType {
175
+ func toUIFeedbackType() -> UINotificationFeedbackGenerator.FeedbackType {
176
+ switch self {
177
+ case .error:
178
+ return .error
179
+ case .success:
180
+ return .success
181
+ case .warning:
182
+ return .warning
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,404 @@
1
+ //
2
+ // TickleUtils.swift
3
+ // Pods
4
+ //
5
+ // Created by Alireza Hadjar on 9/14/25.
6
+ //
7
+
8
+ import SwiftUI
9
+ import CoreHaptics
10
+
11
+ public enum HapticFeedbackManager {
12
+ public static func generateFeedback(style: UIImpactFeedbackGenerator.FeedbackStyle = .light) {
13
+ let impactGenerator = UIImpactFeedbackGenerator(style: style)
14
+ impactGenerator.prepare()
15
+ impactGenerator.impactOccurred()
16
+ }
17
+ }
18
+
19
+ protocol HapticAnimationState {
20
+ var hapticEvents: [CHHapticEvent] { get }
21
+ var hapticCurves: [CHHapticParameterCurve] { get }
22
+ }
23
+
24
+ /// Stores the configuration for a continuous player so it can be recreated after engine reinitialization
25
+ struct ContinuousPlayerConfig {
26
+ let playerId: String
27
+ let initialIntensity: Float
28
+ let initialSharpness: Float
29
+ }
30
+
31
+ class HapticFeedback {
32
+ static let shared = HapticFeedback()
33
+
34
+ private static let hapticsEnabledKey = "com.tickle.hapticsEnabled"
35
+
36
+ private var engine: CHHapticEngine?
37
+ private var hapticPlayers: [String: CHHapticAdvancedPatternPlayer] = [:]
38
+ private var continuousPlayers: [String: CHHapticAdvancedPatternPlayer] = [:]
39
+ /// Stores configurations for continuous players so they can be recreated after engine reinitialization
40
+ private var continuousPlayerConfigs: [String: ContinuousPlayerConfig] = [:]
41
+ private let lock = NSLock()
42
+
43
+ public var supportsHaptics: Bool {
44
+ return CHHapticEngine.capabilitiesForHardware().supportsHaptics
45
+ }
46
+
47
+ // MARK: - Global Haptics Enable/Disable
48
+
49
+ /// Returns whether haptics are globally enabled. Persisted in UserDefaults.
50
+ /// Defaults to true if not previously set.
51
+ public var hapticsEnabled: Bool {
52
+ get {
53
+ // If key doesn't exist, default to true (haptics enabled)
54
+ if UserDefaults.standard.object(forKey: HapticFeedback.hapticsEnabledKey) == nil {
55
+ return true
56
+ }
57
+ return UserDefaults.standard.bool(forKey: HapticFeedback.hapticsEnabledKey)
58
+ }
59
+ set {
60
+ UserDefaults.standard.set(newValue, forKey: HapticFeedback.hapticsEnabledKey)
61
+ }
62
+ }
63
+
64
+ public func createAndStartHapticEngine() {
65
+ guard supportsHaptics else {
66
+ print("Device does not support haptics")
67
+ return
68
+ }
69
+
70
+ lock.lock()
71
+
72
+ // Clean up existing engine first
73
+ if engine != nil {
74
+ engine?.stop(completionHandler: nil)
75
+ engine = nil
76
+ }
77
+
78
+ // Create and configure a haptic engine.
79
+ do {
80
+ engine = try CHHapticEngine()
81
+ } catch let error {
82
+ print("Engine Creation Error: \(error)")
83
+ lock.unlock()
84
+ return
85
+ }
86
+
87
+ // Mute audio to reduce latency for collision haptics.
88
+ engine?.playsHapticsOnly = true
89
+
90
+ // The stopped handler alerts you of engine stoppage.
91
+ engine?.stoppedHandler = { reason in
92
+ print("Stop Handler: The engine stopped for reason: \(reason.rawValue)")
93
+ switch reason {
94
+ case .audioSessionInterrupt:
95
+ print("Audio session interrupt")
96
+ case .applicationSuspended:
97
+ print("Application suspended")
98
+ case .idleTimeout:
99
+ print("Idle timeout")
100
+ case .systemError:
101
+ print("System error")
102
+ case .notifyWhenFinished:
103
+ print("Playback finished")
104
+ case .gameControllerDisconnect:
105
+ print("Controller disconnected.")
106
+ case .engineDestroyed:
107
+ print("Engine destroyed.")
108
+ @unknown default:
109
+ print("Unknown error")
110
+ }
111
+ }
112
+
113
+ // The reset handler provides an opportunity to restart the engine.
114
+ engine?.resetHandler = { [weak self] in
115
+ print("Reset Handler: Restarting the engine.")
116
+ do {
117
+ // Try restarting the engine.
118
+ try self?.engine?.start()
119
+ } catch {
120
+ print("Failed to start the engine")
121
+ }
122
+ }
123
+
124
+ do {
125
+ try engine?.start()
126
+ } catch {
127
+ print("Failed to start the engine: \(error)")
128
+ }
129
+
130
+ // Copy configs while holding the lock, then release before recreating players
131
+ let configsCopy = Array(continuousPlayerConfigs.values)
132
+ lock.unlock()
133
+
134
+ // Recreate any continuous players that were registered before engine was destroyed
135
+ for config in configsCopy {
136
+ createContinuousPlayerInternal(
137
+ playerId: config.playerId,
138
+ initialIntensity: config.initialIntensity,
139
+ initialSharpness: config.initialSharpness
140
+ )
141
+ }
142
+ }
143
+
144
+ public func createHapticPlayers<State: HapticAnimationState>(for states: [State]) {
145
+ lock.lock()
146
+ defer { lock.unlock() }
147
+
148
+ for state in states {
149
+ let key = String(describing: state)
150
+ guard hapticPlayers[key] == nil else { continue }
151
+ do {
152
+ let pattern = try CHHapticPattern(events: state.hapticEvents, parameterCurves: state.hapticCurves)
153
+ let player = try engine?.makeAdvancedPlayer(with: pattern)
154
+ hapticPlayers[key] = player
155
+ } catch let error {
156
+ print("Haptic Player Creation Error: \(error)")
157
+ }
158
+ }
159
+ }
160
+
161
+ public func clearHapticPlayers() {
162
+ lock.lock()
163
+ defer { lock.unlock() }
164
+
165
+ // Stop all regular haptic players
166
+ for (key, player) in hapticPlayers {
167
+ do {
168
+ try player.stop(atTime: CHHapticTimeImmediate)
169
+ } catch let error {
170
+ print("Error stopping haptic player \(key): \(error)")
171
+ }
172
+ }
173
+ hapticPlayers = [:]
174
+
175
+ // Stop all continuous players
176
+ for (key, player) in continuousPlayers {
177
+ do {
178
+ try player.stop(atTime: CHHapticTimeImmediate)
179
+ } catch let error {
180
+ print("Error stopping continuous player \(key): \(error)")
181
+ }
182
+ }
183
+ continuousPlayers = [:]
184
+ }
185
+
186
+ public func destroyEngine() {
187
+ clearHapticPlayers()
188
+
189
+ lock.lock()
190
+ defer { lock.unlock() }
191
+
192
+ engine?.stop(completionHandler: nil)
193
+ engine = nil
194
+ }
195
+
196
+ public func startHapticPlayer<State: HapticAnimationState>(for state: State) {
197
+ guard hapticsEnabled else { return }
198
+
199
+ lock.lock()
200
+ defer { lock.unlock() }
201
+
202
+ let key = String(describing: state)
203
+ do {
204
+ try hapticPlayers[key]?.start(atTime: CHHapticTimeImmediate)
205
+ } catch let error {
206
+ print("Error starting the haptic player: \(error)")
207
+ }
208
+ }
209
+
210
+ public func stopHapticPlayer<State: HapticAnimationState>(for state: State) {
211
+ lock.lock()
212
+ defer { lock.unlock() }
213
+
214
+ let key = String(describing: state)
215
+ do {
216
+ try hapticPlayers[key]?.stop(atTime: CHHapticTimeImmediate)
217
+ } catch let error {
218
+ print("Error stopping the haptic player: \(error)")
219
+ }
220
+ }
221
+
222
+ public func stopAllHapticPlayers() {
223
+ lock.lock()
224
+ defer { lock.unlock() }
225
+
226
+ for (key, player) in hapticPlayers {
227
+ do {
228
+ try player.stop(atTime: CHHapticTimeImmediate)
229
+ } catch let error {
230
+ print("Error stopping haptic player \(key): \(error)")
231
+ }
232
+ }
233
+ }
234
+
235
+ // MARK: - Continuous Player Methods
236
+
237
+ /// Creates a continuous haptic player with the given ID.
238
+ /// - If a player with this ID already exists, it will be stopped and replaced.
239
+ /// - If the engine is not initialized or device doesn't support haptics, this is a no-op.
240
+ /// - The player configuration is stored so it can be recreated after engine reinitialization.
241
+ public func createContinuousPlayer(playerId: String, initialIntensity: Float, initialSharpness: Float) {
242
+ // Store the configuration so we can recreate the player after engine reinitialization
243
+ let config = ContinuousPlayerConfig(
244
+ playerId: playerId,
245
+ initialIntensity: initialIntensity,
246
+ initialSharpness: initialSharpness
247
+ )
248
+
249
+ lock.lock()
250
+ continuousPlayerConfigs[playerId] = config
251
+ lock.unlock()
252
+
253
+ createContinuousPlayerInternal(
254
+ playerId: playerId,
255
+ initialIntensity: initialIntensity,
256
+ initialSharpness: initialSharpness
257
+ )
258
+ }
259
+
260
+ /// Internal method to create a continuous player without storing config (used during recreation)
261
+ private func createContinuousPlayerInternal(playerId: String, initialIntensity: Float, initialSharpness: Float) {
262
+ lock.lock()
263
+ defer { lock.unlock() }
264
+
265
+ // Guard: Check if engine exists
266
+ guard let engine = engine else {
267
+ print("[Tickle] Cannot create continuous player '\(playerId)': Engine not initialized. Call initializeEngine() first.")
268
+ return
269
+ }
270
+
271
+ // If player already exists with this ID, destroy it first
272
+ if let existingPlayer = continuousPlayers[playerId] {
273
+ do {
274
+ try existingPlayer.stop(atTime: CHHapticTimeImmediate)
275
+ } catch {}
276
+ continuousPlayers.removeValue(forKey: playerId)
277
+ }
278
+
279
+ do {
280
+ // Create an intensity parameter
281
+ let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: initialIntensity)
282
+
283
+ // Create a sharpness parameter
284
+ let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: initialSharpness)
285
+
286
+ // Create a continuous event with a long duration
287
+ let continuousEvent = CHHapticEvent(
288
+ eventType: .hapticContinuous,
289
+ parameters: [intensity, sharpness],
290
+ relativeTime: 0,
291
+ duration: 30000
292
+ )
293
+
294
+ // Create a pattern from the continuous haptic event
295
+ let pattern = try CHHapticPattern(events: [continuousEvent], parameters: [])
296
+
297
+ // Create a player from the continuous haptic pattern
298
+ let player = try engine.makeAdvancedPlayer(with: pattern)
299
+ continuousPlayers[playerId] = player
300
+ } catch let error {
301
+ print("[Tickle] Continuous player '\(playerId)' creation error: \(error)")
302
+ }
303
+ }
304
+
305
+ /// Starts the continuous haptic player with the given ID.
306
+ /// - If the player doesn't exist (not created or already destroyed), this is a safe no-op.
307
+ /// - If haptics are globally disabled, this is a no-op.
308
+ public func startContinuousPlayer(playerId: String) {
309
+ guard hapticsEnabled else { return }
310
+
311
+ lock.lock()
312
+ defer { lock.unlock() }
313
+
314
+ guard let player = continuousPlayers[playerId] else {
315
+ // Player doesn't exist - this is safe, just a no-op
316
+ // This can happen if start is called before create, or after destroy
317
+ return
318
+ }
319
+
320
+ do {
321
+ try player.start(atTime: CHHapticTimeImmediate)
322
+ } catch let error {
323
+ print("[Tickle] Error starting continuous player '\(playerId)': \(error)")
324
+ }
325
+ }
326
+
327
+ /// Updates the continuous haptic player parameters.
328
+ /// - If the player doesn't exist, this is a safe no-op.
329
+ /// - Parameters can be updated even before start() - they will take effect when started.
330
+ public func updateContinuousPlayer(playerId: String, intensityControl: Float, sharpnessControl: Float) {
331
+ lock.lock()
332
+ defer { lock.unlock() }
333
+
334
+ guard let player = continuousPlayers[playerId] else {
335
+ // Player doesn't exist - safe no-op
336
+ return
337
+ }
338
+
339
+ // Create dynamic parameters for the updated intensity & sharpness
340
+ let intensityParameter = CHHapticDynamicParameter(
341
+ parameterID: .hapticIntensityControl,
342
+ value: intensityControl,
343
+ relativeTime: 0
344
+ )
345
+
346
+ let sharpnessParameter = CHHapticDynamicParameter(
347
+ parameterID: .hapticSharpnessControl,
348
+ value: sharpnessControl,
349
+ relativeTime: 0
350
+ )
351
+
352
+ // Send dynamic parameters to the haptic player
353
+ do {
354
+ try player.sendParameters([intensityParameter, sharpnessParameter], atTime: 0)
355
+ } catch let error {
356
+ print("[Tickle] Dynamic parameter error for player '\(playerId)': \(error)")
357
+ }
358
+ }
359
+
360
+ /// Stops the continuous haptic player.
361
+ /// - If the player doesn't exist or is already stopped, this is a safe no-op.
362
+ public func stopContinuousPlayer(playerId: String) {
363
+ lock.lock()
364
+ defer { lock.unlock() }
365
+
366
+ guard let player = continuousPlayers[playerId] else {
367
+ // Player doesn't exist - safe no-op
368
+ return
369
+ }
370
+
371
+ do {
372
+ try player.stop(atTime: CHHapticTimeImmediate)
373
+ } catch let error {
374
+ print("[Tickle] Error stopping continuous player '\(playerId)': \(error)")
375
+ }
376
+ }
377
+
378
+ /// Destroys the continuous haptic player and releases resources.
379
+ /// - If the player doesn't exist (not created or already destroyed), this is a safe no-op.
380
+ /// - The player will be stopped if it's currently playing.
381
+ /// - The player configuration is also removed, so it won't be recreated after engine reinitialization.
382
+ public func destroyContinuousPlayer(playerId: String) {
383
+ lock.lock()
384
+ defer { lock.unlock() }
385
+
386
+ // Remove the stored config so this player won't be recreated after engine reinitialization
387
+ continuousPlayerConfigs.removeValue(forKey: playerId)
388
+
389
+ guard let player = continuousPlayers[playerId] else {
390
+ // Player doesn't exist - safe no-op
391
+ // This can happen if destroy is called before create, or called multiple times
392
+ return
393
+ }
394
+
395
+ do {
396
+ try player.stop(atTime: CHHapticTimeImmediate)
397
+ } catch {
398
+ // Ignore stop errors during destroy - player might already be stopped
399
+ }
400
+ continuousPlayers.removeValue(forKey: playerId)
401
+ }
402
+ }
403
+
404
+ let haptics = HapticFeedback.shared
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=Tickle.nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["Tickle.nitro.ts"],"mappings":"","ignoreList":[]}