@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.
- package/LICENSE +21 -0
- package/README.md +352 -0
- package/Tickle.podspec +29 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +126 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/tickle/Tickle.kt +71 -0
- package/android/src/main/java/com/margelo/nitro/tickle/TicklePackage.kt +22 -0
- package/ios/Tickle.swift +185 -0
- package/ios/TickleUtils.swift +404 -0
- package/lib/module/Tickle.nitro.js +4 -0
- package/lib/module/Tickle.nitro.js.map +1 -0
- package/lib/module/index.js +254 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Tickle.nitro.d.ts +63 -0
- package/lib/typescript/src/Tickle.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +148 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JHapticCurve.hpp +87 -0
- package/nitrogen/generated/android/c++/JHapticCurveControlPoint.hpp +61 -0
- package/nitrogen/generated/android/c++/JHapticEvent.hpp +94 -0
- package/nitrogen/generated/android/c++/JHapticEventParameter.hpp +62 -0
- package/nitrogen/generated/android/c++/JHapticEventType.hpp +58 -0
- package/nitrogen/generated/android/c++/JHapticImpactStyle.hpp +67 -0
- package/nitrogen/generated/android/c++/JHapticNotificationType.hpp +61 -0
- package/nitrogen/generated/android/c++/JHapticParameterType.hpp +58 -0
- package/nitrogen/generated/android/c++/JHybridTickleSpec.cpp +162 -0
- package/nitrogen/generated/android/c++/JHybridTickleSpec.hpp +79 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticCurve.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticCurveControlPoint.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticEvent.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticEventParameter.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticEventType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticImpactStyle.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticNotificationType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HapticParameterType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/HybridTickleSpec.kt +109 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/tickle/tickleOnLoad.kt +35 -0
- package/nitrogen/generated/android/tickle+autolinking.cmake +81 -0
- package/nitrogen/generated/android/tickle+autolinking.gradle +27 -0
- package/nitrogen/generated/android/tickleOnLoad.cpp +44 -0
- package/nitrogen/generated/android/tickleOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/Tickle+autolinking.rb +60 -0
- package/nitrogen/generated/ios/Tickle-Swift-Cxx-Bridge.cpp +33 -0
- package/nitrogen/generated/ios/Tickle-Swift-Cxx-Bridge.hpp +139 -0
- package/nitrogen/generated/ios/Tickle-Swift-Cxx-Umbrella.hpp +70 -0
- package/nitrogen/generated/ios/TickleAutolinking.mm +33 -0
- package/nitrogen/generated/ios/TickleAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridTickleSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridTickleSpecSwift.hpp +185 -0
- package/nitrogen/generated/ios/swift/HapticCurve.swift +46 -0
- package/nitrogen/generated/ios/swift/HapticCurveControlPoint.swift +35 -0
- package/nitrogen/generated/ios/swift/HapticEvent.swift +57 -0
- package/nitrogen/generated/ios/swift/HapticEventParameter.swift +35 -0
- package/nitrogen/generated/ios/swift/HapticEventType.swift +40 -0
- package/nitrogen/generated/ios/swift/HapticImpactStyle.swift +52 -0
- package/nitrogen/generated/ios/swift/HapticNotificationType.swift +44 -0
- package/nitrogen/generated/ios/swift/HapticParameterType.swift +40 -0
- package/nitrogen/generated/ios/swift/HybridTickleSpec.swift +69 -0
- package/nitrogen/generated/ios/swift/HybridTickleSpec_cxx.swift +282 -0
- package/nitrogen/generated/shared/c++/HapticCurve.hpp +96 -0
- package/nitrogen/generated/shared/c++/HapticCurveControlPoint.hpp +87 -0
- package/nitrogen/generated/shared/c++/HapticEvent.hpp +101 -0
- package/nitrogen/generated/shared/c++/HapticEventParameter.hpp +88 -0
- package/nitrogen/generated/shared/c++/HapticEventType.hpp +76 -0
- package/nitrogen/generated/shared/c++/HapticImpactStyle.hpp +88 -0
- package/nitrogen/generated/shared/c++/HapticNotificationType.hpp +80 -0
- package/nitrogen/generated/shared/c++/HapticParameterType.hpp +76 -0
- package/nitrogen/generated/shared/c++/HybridTickleSpec.cpp +34 -0
- package/nitrogen/generated/shared/c++/HybridTickleSpec.hpp +87 -0
- package/package.json +179 -0
- package/react-native.config.js +8 -0
- package/src/Tickle.nitro.ts +84 -0
- package/src/index.tsx +306 -0
package/ios/Tickle.swift
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["Tickle.nitro.ts"],"mappings":"","ignoreList":[]}
|