Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 43 additions & 50 deletions extension/haptics/Haptic.hx
Original file line number Diff line number Diff line change
@@ -1,46 +1,67 @@
package extension.haptics;

import extension.haptics.backends.IHapticBackend;
#if android
import extension.haptics.android.HapticAndroid;
import extension.haptics.backends.AndroidHapticBackend;
#elseif ios
import extension.haptics.ios.HapticIOS;
import extension.haptics.backends.IOSHapticBackend;
#end

/**
* This class provides a cross-platform interface for haptic feedback functionality.
* Can be safely accessed cross platform, as any unsupported platforms (desktop/html5) simply just call empty backend functions
* So usage is generally
* Haptic.initialize();
* Haptic.vibrateOneShot(0.5, 0.2, 0.8); // Calls a one-shot vibration that lasts 0.5 seconds, at 0.2 intensity, and at 0.8 sharpness
*
* #if ios
* // iOS specific functions are in HapticIOS.hx
* HapticIOS.vibratePatternFromData(Assets.getBytes("data/heartbeats.ahap"));
* #end
*
* #if android
* // Android specific functions are in HapticAndroid.hx
* if(HapticAndroid.isPrimitiveSupported(HapticAndroid.PRIMITIVE_CLICK))
* trace("Has Android Click Primitive!");
* #end
*/
class Haptic
{

private static var _backend:Null<IHapticBackend>;
public static var backend(get, never):IHapticBackend;

static function get_backend():IHapticBackend
{
if (_backend == null) _backend = createBackend();
return _backend;
}

static function createBackend():IHapticBackend
{
#if android
return new AndroidHapticBackend();
#elseif ios
return new IOSHapticBackend();
#else
return new EmptyHapticBackend();
#end
}

/**
* Initializes the haptic system for the current platform.
*
* This method delegates to the platform-specific implementation:
* - Android: Calls `HapticAndroid.initialize()`.
* - iOS: Calls `HapticIOS.initialize()`.
*/
public static function initialize():Void
{
#if android
HapticAndroid.initialize();
#elseif ios
HapticIOS.initialize();
#end
backend.initialize();
}

/**
* Disposes of the haptic system for the current platform.
*
* This method delegates to the platform-specific implementation:
* - Android: Calls `HapticAndroid.dispose()`.
* - iOS: Calls `HapticIOS.dispose()`.
*/
public static function dispose():Void
{
#if android
HapticAndroid.dispose();
#elseif ios
HapticIOS.dispose();
#end
backend.dispose();
}

/**
Expand All @@ -56,11 +77,7 @@ class Haptic
*/
public static function vibrateOneShot(duration:Float, amplitude:Float, sharpness:Float):Void
{
#if android
HapticAndroid.vibrateOneShot(Math.floor(duration * 1000), Math.floor(Math.max(1, Math.min(255, amplitude * 255))));
#elseif ios
HapticIOS.vibrateOneShot(duration, Math.max(0, Math.min(1, amplitude)), Math.max(0, Math.min(1, sharpness)));
#end
backend.vibrateOneShot(duration, amplitude, sharpness);
}

/**
Expand All @@ -76,30 +93,6 @@ class Haptic
*/
public static function vibratePattern(durations:Array<Float>, amplitudes:Array<Float>, sharpnesses:Array<Float>):Void
{
#if android
final intTimings:Array<Int> = [0]; // The first element is the delay.

for (i in 0...durations.length)
intTimings.push(Math.floor(durations[i] * 1000));

final intAmplitudes:Array<Int> = [0]; // Since the first element is the delay, it won't have an amplitude.

for (i in 0...amplitudes.length)
intAmplitudes.push(Math.floor(Math.max(1, Math.min(255, amplitudes[i] * 255))));

HapticAndroid.vibratePattern(intTimings, intAmplitudes);
#elseif ios
final singleAmplitudes:Array<Single> = [];

for (i in 0...amplitudes.length)
singleAmplitudes[i] = (amplitudes[i] : Single);

final singleSharpnesses:Array<Single> = [];

for (i in 0...sharpnesses.length)
singleSharpnesses[i] = (sharpnesses[i] : Single);

HapticIOS.vibratePattern(durations, singleAmplitudes, singleSharpnesses);
#end
backend.vibratePattern(durations, amplitudes, sharpnesses);
}
}
85 changes: 85 additions & 0 deletions extension/haptics/HapticAndroid.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package extension.haptics;

#if android
import extension.haptics.backends.AndroidHapticBackend;

/**
* Android specific haptics functions
* Note: Make sure `Haptic.initialize()` gets called before you attempt using these!
*/
class HapticAndroid
{
/**
* Represents a short, sharp click vibration.
*/
public static final PRIMITIVE_CLICK:Int = 1;

/**
* Represents a low-intensity tick vibration.
*/
public static final PRIMITIVE_LOW_TICK:Int = 8;

/**
* Represents a quick, falling vibration pattern.
*/
public static final PRIMITIVE_QUICK_FALL:Int = 6;

/**
* Represents a quick, rising vibration pattern.
*/
public static final PRIMITIVE_QUICK_RISE:Int = 4;

/**
* Represents a slow, rising vibration pattern.
*/
public static final PRIMITIVE_SLOW_RISE:Int = 5;

/**
* Represents a spinning vibration pattern.
*/
public static final PRIMITIVE_SPIN:Int = 3;

/**
* Represents a heavy, thud-like vibration.
*/
public static final PRIMITIVE_THUD:Int = 2;

/**
* Represents a standard tick vibration.
*/
public static final PRIMITIVE_TICK:Int = 7;

/**
* Checks if a specific vibration primitive is supported.
*
* @param primitiveId The ID of the vibration primitive.
*
* @return `true` if the primitive is supported, false otherwise.
*/
public static function isPrimitiveSupported(primitiveId:Int):Bool
{
@:privateAccess
final isPrimitiveSupportedJNI:Null<Dynamic> = AndroidHapticBackend.createJNIStaticMethod('org/haxe/extension/Haptic', 'isPrimitiveSupported', '(I)Z');

if (isPrimitiveSupportedJNI != null)
return isPrimitiveSupportedJNI(primitiveId);

return false;
}

/**
* Triggers a predefined vibration pattern using primitive IDs, scales, and delays.
*
* @param primitiveIds An array of integers representing predefined vibration primitive IDs.
* @param scales An array of scaling factors (0.0 to 1.0) for the intensity of each primitive.
* @param delays An array of delays in milliseconds before each primitive is triggered.
*/
public static function vibratePredefined(primitiveIds:Array<Int>, scales:Array<Float>, delays:Array<Int>):Void
{
@:privateAccess
final vibratePredefinedJNI:Null<Dynamic> = AndroidHapticBackend.createJNIStaticMethod('org/haxe/extension/Haptic', 'vibratePredefined', '([I[D[I)V');

if (vibratePredefinedJNI != null)
vibratePredefinedJNI(primitiveIds, scales, delays);
}
}
33 changes: 33 additions & 0 deletions extension/haptics/HapticIOS.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package extension.haptics;

import extension.haptics.backends.IOSHapticBackend;
#if ios
/**
* IOS specific haptics functions and features
* Note: Make sure `Haptic.initialize()` gets called before you attempt using these!
*/
@:buildXml('<include name="${haxelib:extension-haptics}/project/haptic-ios/Build.xml" />')
@:headerInclude('haptic.hpp')
class HapticIOS
{
/**
* Triggers a haptic vibration pattern using the provided pattern data.
*
* @param data The AHAP pattern data as a `Bytes` object.
*/
public static function vibratePatternFromData(data:Bytes):Void
{
if (data != null)
hapticVibratePatternFromData(cast cpp.Pointer.arrayElem(data.getData(), 0).constRaw, data.length);
}

/**
* Native function to triggers a pattern vibration using a pattern file (.ahap).
*
* @param bytes The buffer containing data for the new object.
* @param length The number of bytes to copy from bytes.
*/
@:native('hapticVibratePatternFromData')
extern public static function hapticVibratePatternFromData(bytes:cpp.RawConstPointer<cpp.Void>, len:cpp.SizeT):Void;
}
#end
Original file line number Diff line number Diff line change
@@ -1,62 +1,25 @@
package extension.haptics.android;
package extension.haptics.backend;

#if android
import lime.system.JNI;
using Lambda;

/**
* This class provides haptic feedback functionality for `Android` devices using native `Java` methods.
*
* @see https://developer.android.com/reference/android/os/VibratorManager
* @see https://developer.android.com/reference/android/os/Vibrator
*/
class HapticAndroid
class AndroidHapticBackend implements IHapticBackend
{
/**
* Represents a short, sharp click vibration.
*/
public static final PRIMITIVE_CLICK:Int = 1;

/**
* Represents a low-intensity tick vibration.
*/
public static final PRIMITIVE_LOW_TICK:Int = 8;

/**
* Represents a quick, falling vibration pattern.
*/
public static final PRIMITIVE_QUICK_FALL:Int = 6;

/**
* Represents a quick, rising vibration pattern.
*/
public static final PRIMITIVE_QUICK_RISE:Int = 4;

/**
* Represents a slow, rising vibration pattern.
*/
public static final PRIMITIVE_SLOW_RISE:Int = 5;

/**
* Represents a spinning vibration pattern.
*/
public static final PRIMITIVE_SPIN:Int = 3;

/**
* Represents a heavy, thud-like vibration.
*/
public static final PRIMITIVE_THUD:Int = 2;

/**
* Represents a standard tick vibration.
*/
public static final PRIMITIVE_TICK:Int = 7;


/**
* Cache for storing created static JNI method references.
*/
@:noCompletion
private static var staticMethodsCache:Map<String, Dynamic> = [];

public function new():Void {}
/**
* Initializes the haptic system by calling the corresponding Java method.
*/
Expand Down Expand Up @@ -84,60 +47,42 @@ class HapticAndroid
*
* @param duration The vibration duration in milliseconds.
* @param amplitude The intensity of the vibration (0-255).
* @param sharpness The sharpness of the vibration (0.0 to 1.0, iOS only).
*/
public static function vibrateOneShot(duration:Int, amplitude:Int):Void
public static function vibrateOneShot(duration:Float, amplitude:Float, sharpness:Float):Void
{
final vibrateJNI:Null<Dynamic> = createJNIStaticMethod('org/haxe/extension/Haptic', 'vibrateOneShot', '(II)V');

if (vibrateJNI != null)
vibrateJNI(duration, amplitude);
vibrateJNI(Math.floor(duration * 1000), Math.floor(Math.max(1, Math.min(255, amplitude * 255))));
}

/**
* Triggers a pattern vibration defined by arrays of timings and amplitudes.
*
* @param timings An array of vibration durations in milliseconds.
* @param durations An array of vibration durations in milliseconds.
* @param amplitudes An array of vibration intensities (0-255).
* @param sharpnesses unused on Android
*/
public static function vibratePattern(timings:Array<Int>, amplitudes:Array<Int>):Void
public static function vibratePattern(durations:Array<Float>, amplitudes:Array<Float>, sharpnesses:Array<Float>):Void
{
final vibratePatternJNI:Null<Dynamic> = createJNIStaticMethod('org/haxe/extension/Haptic', 'vibratePattern', '([I[I)V');
function durationToMilliseconds(duration:Float):Int return Std.int(Math.floor(duration * 1000));
// scales amplitude between 0 and 255 for android amplitude input
function rescaleAmplitudes(amplitude:Float):Int return Std.int(Math.floor(Math.max(1, Math.min(255, amplitude * 255))));

if (vibratePatternJNI != null)
vibratePatternJNI(timings, amplitudes);
}
// on Android, the first element is the delay. We want to start immediately, so 0
durations.unshift(0);
amplitudes.unshift(0);

/**
* Checks if a specific vibration primitive is supported.
*
* @param primitiveId The ID of the vibration primitive.
*
* @return `true` if the primitive is supported, false otherwise.
*/
public static function isPrimitiveSupported(primitiveId:Int):Bool
{
final isPrimitiveSupportedJNI:Null<Dynamic> = createJNIStaticMethod('org/haxe/extension/Haptic', 'isPrimitiveSupported', '(I)Z');
durations = durations.map(durationToMilliseconds);
amplitudes = amplitudes.map(rescaleAmplitudes);

if (isPrimitiveSupportedJNI != null)
return isPrimitiveSupportedJNI(primitiveId);
final vibratePatternJNI:Null<Dynamic> = createJNIStaticMethod('org/haxe/extension/Haptic', 'vibratePattern', '([I[I)V');

return false;
if (vibratePatternJNI != null)
vibratePatternJNI(durations, amplitudes);
}

/**
* Triggers a predefined vibration pattern using primitive IDs, scales, and delays.
*
* @param primitiveIds An array of integers representing predefined vibration primitive IDs.
* @param scales An array of scaling factors (0.0 to 1.0) for the intensity of each primitive.
* @param delays An array of delays in milliseconds before each primitive is triggered.
*/
public static function vibratePredefined(primitiveIds:Array<Int>, scales:Array<Float>, delays:Array<Int>):Void
{
final vibratePredefinedJNI:Null<Dynamic> = createJNIStaticMethod('org/haxe/extension/Haptic', 'vibratePredefined', '([I[D[I)V');

if (vibratePredefinedJNI != null)
vibratePredefinedJNI(primitiveIds, scales, delays);
}

/**
* Retrieves or creates a cached static method reference.
Expand Down
Loading