diff --git a/SteamController/Devices/MouseControllerFauxLizard.cs b/SteamController/Devices/MouseControllerFauxLizard.cs deleted file mode 100644 index 0f6d854..0000000 --- a/SteamController/Devices/MouseControllerFauxLizard.cs +++ /dev/null @@ -1,506 +0,0 @@ -using System; -using static SteamController.Devices.SteamController; - -namespace SteamController.Devices -{ - public partial class MouseController : IDisposable - { - // Adjustable parameters (tweakable settings) - private const int bufferSize = 4; // Input smoothing buffer size - private const int gestureRadius = 30; // Gesture commit threshold (pixels) - private const double gestureFlushRate = 0.1; // Rate at which gesture flush decays - private const int velocityWindowSize = 20; // Velocity smoothing window size - private const double minGlideMagnitude = 0.8; // Minimum velocity to start glide - private const double minGlideVelocity = 0.15; // Minimum velocity to continue glide - private const int hapticBufferSize = 10; // Haptic smoothing and jitter reduction buffer - private const double hapticTriggerDelta = 30; // Distance required for haptic feedback on each pad - private const int hapticResetTime = 5; // Number of seconds to invalidate a haptic feedback, prevents triggering on drag jitter - - // Functional constants (derived once) - private readonly double gestureRadiusSq = gestureRadius * gestureRadius; - private readonly double minGlideMagnitudeSq = minGlideMagnitude * minGlideMagnitude; - private readonly double minGlideVelocitySq = minGlideVelocity * minGlideVelocity; - private readonly double hapticBufferMidpoint = hapticBufferSize / 2.0; - - // Runtime state RPad - private readonly double[] bufX = new double[bufferSize]; - private int bufXIndex = 0, bufXCount = 0; - private double bufXSum = 0; - private readonly double[] bufY = new double[bufferSize]; - private int bufYIndex = 0, bufYCount = 0; - private double bufYSum = 0; - private double totalDeltaX = 0, totalDeltaY = 0; - private bool gestureCommitted = false; - private double gestureFlushX = 0, gestureFlushY = 0; - - private bool isGliding = false; - private long releaseTicks; - private double releaseVelocityX = 0, releaseVelocityY = 0; - private readonly double[] velocityX = new double[velocityWindowSize]; - private int velocityXIndex = 0, velocityXCount = 0; - private double velocityXSum = 0; - private readonly double[] velocityY = new double[velocityWindowSize]; - private int velocityYIndex = 0, velocityYCount = 0; - private double velocityYSum = 0; - - // Runtime state haptics - // Right track pad - private readonly int[] hapticDxSignBufferR = new int[hapticBufferSize]; - private readonly int[] hapticDySignBufferR = new int[hapticBufferSize]; - private readonly int[] hapticDxFlipFlagBufferR = new int[hapticBufferSize]; - private readonly int[] hapticDyFlipFlagBufferR = new int[hapticBufferSize]; - private readonly double[] hapticMagBufferR = new double[hapticBufferSize]; - - private double hapticDeltaR = 0; - private int hapticBufferCountR = 0; - private int hapticBufferIndexR = 0; - private int hapticDxFlipCountR = 0; - private int hapticDyFlipCountR = 0; - private int hapticDxZeroCountR = 0; - private int hapticDyZeroCountR = 0; - private double hapticMagSumR = 0; - private DateTime hapticLastTimeR = DateTime.MinValue; - - private bool hapticClearedR = true; - - // Left track pad - private readonly int[] hapticDxSignBufferL = new int[hapticBufferSize]; - private readonly int[] hapticDySignBufferL = new int[hapticBufferSize]; - private readonly int[] hapticDxFlipFlagBufferL = new int[hapticBufferSize]; - private readonly int[] hapticDyFlipFlagBufferL = new int[hapticBufferSize]; - private readonly double[] hapticMagBufferL = new double[hapticBufferSize]; - - private double hapticDeltaL = 0; - private int hapticBufferCountL = 0; - private int hapticBufferIndexL = 0; - private int hapticDxFlipCountL = 0; - private int hapticDyFlipCountL = 0; - private int hapticDxZeroCountL = 0; - private int hapticDyZeroCountL = 0; - private double hapticMagSumL = 0; - private DateTime hapticLastTimeL = DateTime.MinValue; - - private bool hapticClearedL = true; - - // Main movement logic - public void MoveByFauxLizard(double dx, double dy, bool isTouched) - { - if (!isTouched) - { - // Start glide if gesture was committed and velocity is high enough - if (!isGliding && gestureCommitted) - { - double magSq = releaseVelocityX * releaseVelocityX + releaseVelocityY * releaseVelocityY; - if (magSq >= minGlideMagnitudeSq) - { - releaseTicks = System.Diagnostics.Stopwatch.GetTimestamp(); - isGliding = true; - } - } - - // Continue glide if active - if (isGliding) - { - double elapsed = - (System.Diagnostics.Stopwatch.GetTimestamp() - releaseTicks) - / (double)System.Diagnostics.Stopwatch.Frequency; - double glideX = ApplyReleaseGlide(releaseVelocityX, elapsed); - double glideY = ApplyReleaseGlide(releaseVelocityY, elapsed); - - double glideMagSq = glideX * glideX + glideY * glideY; - if (glideMagSq < minGlideVelocitySq) - { - isGliding = false; - releaseVelocityX = releaseVelocityY = 0; - } - else - { - MoveBy(glideX, glideY); - } - } - - // Reset gesture state - gestureCommitted = false; - bufXIndex = bufXCount = bufYIndex = bufYCount = velocityXIndex = velocityXCount = velocityYIndex = velocityYCount = 0; - bufYSum = bufXSum = velocityXSum = velocityYSum = totalDeltaX = totalDeltaY = 0; - return; - } - - isGliding = false; - - // Smooth input deltas - double smoothedX = SmoothDelta(dx, bufX, ref bufXIndex, ref bufXCount, ref bufXSum); - double smoothedY = SmoothDelta(dy, bufY, ref bufYIndex, ref bufYCount, ref bufYSum); - - // Apply acceleration ramp - double rampedX = ApplyAccelerationRamp(smoothedX); - double rampedY = ApplyAccelerationRamp(smoothedY); - - // Accumulate gesture until committed - if (!gestureCommitted) - { - totalDeltaX += rampedX; - totalDeltaY += rampedY; - - double gestureMagSq = totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY; - if (gestureMagSq > gestureRadiusSq) - { - gestureCommitted = true; - gestureFlushX = totalDeltaX; - gestureFlushY = totalDeltaY; - - velocityXIndex = velocityXCount = 0; velocityXSum = 0; - velocityYIndex = velocityYCount = 0; velocityYSum = 0; - } - else return; - } - - // Track velocity using rolling sum - releaseVelocityX = SmoothVelocity(rampedX, velocityX, ref velocityXIndex, ref velocityXCount, ref velocityXSum); - releaseVelocityY = SmoothVelocity(rampedY, velocityY, ref velocityYIndex, ref velocityYCount, ref velocityYSum); - - // Apply gesture flush - double flushedX = gestureFlushX * gestureFlushRate; - double flushedY = gestureFlushY * gestureFlushRate; - gestureFlushX -= flushedX; - gestureFlushY -= flushedY; - - // Final movement vector - double finalX = flushedX + rampedX; - double finalY = flushedY + rampedY; - - MoveBy(finalX, finalY); - } - - // Checks whether the pads have been dragged enough to trigger a haptic feedback - public bool HapticDragRFauxLizard(double dx, double dy, bool isTouched) - { - if (isTouched) - { - hapticClearedR = false; - - // Timeout check: reset delta if accumulation is too slow - DateTime now = DateTime.UtcNow; - if (hapticBufferCountR == 0) - { - hapticLastTimeR = now; - } - else if ((now - hapticLastTimeR).TotalSeconds > hapticResetTime) - { - hapticDeltaR = 0; - hapticLastTimeR = now; - } - - // If buffer is full, remove array contributions at this slot - if (hapticBufferCountR == hapticBufferSize) - { - int idx = hapticBufferIndexR; - hapticDxFlipCountR -= hapticDxFlipFlagBufferR[idx]; - hapticDyFlipCountR -= hapticDyFlipFlagBufferR[idx]; - if (hapticDxSignBufferR[idx] == 0) hapticDxZeroCountR--; - if (hapticDySignBufferR[idx] == 0) hapticDyZeroCountR--; - hapticMagSumR -= hapticMagBufferR[idx]; - } - else - { - hapticBufferCountR++; - } - - // Compute current signs - int curDxSign = Math.Sign(dx); - int curDySign = Math.Sign(dy); - - // Compute flips against previous signs - int dxFlip = 0, dyFlip = 0; - if (hapticBufferCountR > 1) - { - int prevIndex = (hapticBufferIndexR - 1 + hapticBufferSize) % hapticBufferSize; - int prevDxSign = hapticDxSignBufferR[prevIndex]; - int prevDySign = hapticDySignBufferR[prevIndex]; - - if (prevDxSign != 0 && curDxSign != 0 && prevDxSign != curDxSign) dxFlip = 1; - if (prevDySign != 0 && curDySign != 0 && prevDySign != curDySign) dyFlip = 1; - } - - // Compute current magnitude - double mag = Math.Sqrt(dx * dx + dy * dy); - - // Store signs and flip flags and magnitude in array for rolling sums - int i = hapticBufferIndexR; - hapticDxSignBufferR[i] = curDxSign; - hapticDySignBufferR[i] = curDySign; - hapticDxFlipFlagBufferR[i] = dxFlip; - hapticDyFlipFlagBufferR[i] = dyFlip; - hapticMagBufferR[i] = mag; - - // Increment rolling sums - if (curDxSign == 0) hapticDxZeroCountR++; - if (curDySign == 0) hapticDyZeroCountR++; - hapticDxFlipCountR += dxFlip; - hapticDyFlipCountR += dyFlip; - hapticMagSumR += mag; - - // Store new buffer index for later - hapticBufferIndexR++; - if (hapticBufferIndexR == hapticBufferSize) hapticBufferIndexR = 0; - - // No need to proceed as buffer is not full yet - if (hapticBufferCountR != hapticBufferSize) - return false; - - // Compute average magnitude over buffer for smoothing - double avgMag = hapticMagSumR / hapticBufferCountR; - - // Calculate a penalty for the magnitude - double bufferSpan = hapticBufferSize - hapticBufferMidpoint; - double invSpan = 1.0 / bufferSpan; - bool dxZeroEdge = hapticDxZeroCountR == hapticBufferCountR; - bool dyZeroEdge = hapticDyZeroCountR == hapticBufferCountR; - double factor = (dxZeroEdge || dyZeroEdge) ? 1.0 : 0.5; - - double dxFactor = dxZeroEdge - ? 0.0 - : (hapticDxFlipCountR < hapticBufferMidpoint - ? factor - : -factor * ((hapticDxFlipCountR - hapticBufferMidpoint) * invSpan)); - - double dyFactor = dyZeroEdge - ? 0.0 - : (hapticDyFlipCountR < hapticBufferMidpoint - ? factor - : -factor * ((hapticDyFlipCountR - hapticBufferMidpoint) * invSpan)); - - // Final penalty - hapticDeltaR += avgMag * (dxFactor + dyFactor); - - if (hapticDeltaR >= hapticTriggerDelta) - { - hapticDeltaR -= Math.Floor(hapticDeltaR / hapticTriggerDelta) * hapticTriggerDelta; - hapticLastTimeR = now; - return true; - } - else if (hapticDeltaR < 0) - { - hapticDeltaR = 0; - } - } - else - { - if (!hapticClearedR) - { - hapticClearedR = true; - - // Reset all buffers when not touched - hapticDeltaR = 0; - hapticBufferCountR = 0; - hapticBufferIndexR = 0; - hapticDxFlipCountR = 0; - hapticDyFlipCountR = 0; - hapticDxZeroCountR = 0; - hapticDyZeroCountR = 0; - hapticMagSumR = 0; - } - } - - return false; - } - - public bool HapticDragLFauxLizard(double dx, double dy, bool isTouched) - { - if (isTouched) - { - hapticClearedL = false; - - // Timeout check: reset delta if accumulation is too slow - DateTime now = DateTime.UtcNow; - if (hapticBufferCountL == 0) - { - hapticLastTimeL = now; - } - else if ((now - hapticLastTimeL).TotalSeconds > hapticResetTime) - { - hapticDeltaL = 0; - hapticLastTimeL = now; - } - - // If buffer is full, remove array contributions at this slot - if (hapticBufferCountL == hapticBufferSize) - { - int idx = hapticBufferIndexL; - hapticDxFlipCountL -= hapticDxFlipFlagBufferL[idx]; - hapticDyFlipCountL -= hapticDyFlipFlagBufferL[idx]; - if (hapticDxSignBufferL[idx] == 0) hapticDxZeroCountL--; - if (hapticDySignBufferL[idx] == 0) hapticDyZeroCountL--; - hapticMagSumL -= hapticMagBufferL[idx]; - } - else - { - hapticBufferCountL++; - } - - // Compute current signs - int curDxSign = Math.Sign(dx); - int curDySign = Math.Sign(dy); - - // Compute flips against previous signs - int dxFlip = 0, dyFlip = 0; - if (hapticBufferCountL > 1) - { - int prevIndex = (hapticBufferIndexL - 1 + hapticBufferSize) % hapticBufferSize; - int prevDxSign = hapticDxSignBufferL[prevIndex]; - int prevDySign = hapticDySignBufferL[prevIndex]; - - if (prevDxSign != 0 && curDxSign != 0 && prevDxSign != curDxSign) dxFlip = 1; - if (prevDySign != 0 && curDySign != 0 && prevDySign != curDySign) dyFlip = 1; - } - - // Compute current magnitude - double mag = Math.Sqrt(dx * dx + dy * dy); - - // Store signs and flip flags and magnitude in array for rolling sums - int i = hapticBufferIndexL; - hapticDxSignBufferL[i] = curDxSign; - hapticDySignBufferL[i] = curDySign; - hapticDxFlipFlagBufferL[i] = dxFlip; - hapticDyFlipFlagBufferL[i] = dyFlip; - hapticMagBufferL[i] = mag; - - // Increment rolling sums - if (curDxSign == 0) hapticDxZeroCountL++; - if (curDySign == 0) hapticDyZeroCountL++; - hapticDxFlipCountL += dxFlip; - hapticDyFlipCountL += dyFlip; - hapticMagSumL += mag; - - // Store new buffer index for later - hapticBufferIndexL++; - if (hapticBufferIndexL == hapticBufferSize) hapticBufferIndexL = 0; - - // No need to proceed as buffer is not full yet - if (hapticBufferCountL != hapticBufferSize) - return false; - - // Compute average magnitude over buffer for smoothing - double avgMag = hapticMagSumL / hapticBufferCountL; - - // Calculate a penalty for the magnitude - double bufferSpan = hapticBufferSize - hapticBufferMidpoint; - double invSpan = 1.0 / bufferSpan; - bool dxZeroEdge = hapticDxZeroCountL == hapticBufferCountL; - bool dyZeroEdge = hapticDyZeroCountL == hapticBufferCountL; - double factor = (dxZeroEdge || dyZeroEdge) ? 1.0 : 0.5; - - double dxFactor = dxZeroEdge - ? 0.0 - : (hapticDxFlipCountL < hapticBufferMidpoint - ? factor - : -factor * ((hapticDxFlipCountL - hapticBufferMidpoint) * invSpan)); - - double dyFactor = dyZeroEdge - ? 0.0 - : (hapticDyFlipCountL < hapticBufferMidpoint - ? factor - : -factor * ((hapticDyFlipCountL - hapticBufferMidpoint) * invSpan)); - - // Final penalty - hapticDeltaL += avgMag * (dxFactor + dyFactor); - - if (hapticDeltaL >= hapticTriggerDelta) - { - hapticDeltaL -= Math.Floor(hapticDeltaL / hapticTriggerDelta) * hapticTriggerDelta; - hapticLastTimeL = now; - return true; - } - else if (hapticDeltaL < 0) - { - hapticDeltaL = 0; - } - } - else - { - if (!hapticClearedL) - { - hapticClearedL = true; - - // Reset all buffers when not touched - hapticDeltaL = 0; - hapticBufferCountL = 0; - hapticBufferIndexL = 0; - hapticDxFlipCountL = 0; - hapticDyFlipCountL = 0; - hapticDxZeroCountL = 0; - hapticDyZeroCountL = 0; - hapticMagSumL = 0; - } - } - - return false; - } - - // Input smoothing - private double SmoothDelta(double raw, double[] buffer, ref int index, ref int count, ref double sum) - { - if (count < bufferSize) - { - buffer[index] = raw; - sum += raw; - count++; - } - else - { - sum -= buffer[index]; - buffer[index] = raw; - sum += raw; - } - - index++; - if (index == bufferSize) - index = 0; - - return sum / count; - } - - // Velocity smoothing - private double SmoothVelocity(double v, double[] buffer, ref int index, ref int count, ref double sum) - { - if (count < velocityWindowSize) - { - buffer[index] = v; - sum += v; - count++; - } - else - { - sum -= buffer[index]; - buffer[index] = v; - sum += v; - } - - index++; - if (index == velocityWindowSize) - index = 0; - - return sum / count; - } - - // Acceleration ramp - private double ApplyAccelerationRamp(double delta) - { - double steepness = 0.1; - double midpoint = 6.0; - double maxBoost = 2.0; - double baseBoost = 1.7; - - double absDelta = Math.Abs(delta); - double sigmoidBoost = 1.0 + (maxBoost - 1.0) / (1.0 + Math.Exp(-steepness * (absDelta - midpoint))); - return delta * Math.Max(sigmoidBoost, baseBoost); - } - - // Glide decay - private double ApplyReleaseGlide(double velocity, double elapsed) - { - double rampedRate = 3.0 + Math.Pow(elapsed * 7.0, 2); - return velocity * Math.Exp(-elapsed * rampedRate); - } - } -} \ No newline at end of file diff --git a/SteamController/Devices/MouseControllerFauxLizardHaptic.cs b/SteamController/Devices/MouseControllerFauxLizardHaptic.cs new file mode 100644 index 0000000..07f1ace --- /dev/null +++ b/SteamController/Devices/MouseControllerFauxLizardHaptic.cs @@ -0,0 +1,199 @@ +namespace SteamController.Devices +{ + public partial class MouseController + { + // Adjustable parameters (tweakable settings) + private const int HapticBufferSize = 10; // Haptic smoothing and jitter reduction buffer + private const double HapticTriggerDelta = 30; // Distance required for haptic feedback on each pad + private const int HapticResetTime = 5; // Number of seconds to invalidate a haptic feedback, prevents triggering on drag jitter + + // Functional constants (derived once) + private static readonly double HapticBufferMidpoint = HapticBufferSize / 2.0; + + // Runtime state haptics + private struct HapticState + { + public readonly int[] DxSign; + public readonly int[] DySign; + public readonly int[] DxFlipFlag; + public readonly int[] DyFlipFlag; + public readonly double[] Mag; + + public double Delta; + public int BufferCount; + public int BufferIndex; + public int DxFlipCount; + public int DyFlipCount; + public int DxZeroCount; + public int DyZeroCount; + public double MagSum; + public DateTime LastTime; + public bool Cleared; + + public HapticState(int n) + { + DxSign = new int[n]; + DySign = new int[n]; + DxFlipFlag = new int[n]; + DyFlipFlag = new int[n]; + Mag = new double[n]; + + Delta = 0; + BufferCount = 0; + BufferIndex = 0; + DxFlipCount = 0; + DyFlipCount = 0; + DxZeroCount = 0; + DyZeroCount = 0; + MagSum = 0; + LastTime = DateTime.MinValue; + Cleared = true; + } + + public void ResetBuffers() + { + Delta = 0; + BufferCount = 0; + BufferIndex = 0; + DxFlipCount = 0; + DyFlipCount = 0; + DxZeroCount = 0; + DyZeroCount = 0; + MagSum = 0; + } + } + + private HapticState hapticR = new HapticState(HapticBufferSize); + private HapticState hapticL = new HapticState(HapticBufferSize); + + // Checks whether the pads have been dragged enough to trigger a haptic feedback + public bool HapticDragRFauxLizard(double dx, double dy, bool isTouched) + => HapticDragFauxLizard(ref hapticR, dx, dy, isTouched); + + public bool HapticDragLFauxLizard(double dx, double dy, bool isTouched) + => HapticDragFauxLizard(ref hapticL, dx, dy, isTouched); + + private bool HapticDragFauxLizard(ref HapticState s, double dx, double dy, bool isTouched) + { + if (isTouched) + { + s.Cleared = false; + + // Timeout check: reset delta if accumulation is too slow + DateTime now = DateTime.UtcNow; + if (s.BufferCount == 0) + { + s.LastTime = now; + } + else if ((now - s.LastTime).TotalSeconds > HapticResetTime) + { + s.Delta = 0; + s.LastTime = now; + } + + // If buffer is full, remove array contributions at this slot + if (s.BufferCount == HapticBufferSize) + { + int idx = s.BufferIndex; + s.DxFlipCount -= s.DxFlipFlag[idx]; + s.DyFlipCount -= s.DyFlipFlag[idx]; + if (s.DxSign[idx] == 0) s.DxZeroCount--; + if (s.DySign[idx] == 0) s.DyZeroCount--; + s.MagSum -= s.Mag[idx]; + } + else + { + s.BufferCount++; + } + + // Compute current signs + int curDxSign = Math.Sign(dx); + int curDySign = Math.Sign(dy); + + // Compute flips against previous signs + int dxFlip = 0, dyFlip = 0; + if (s.BufferCount > 1) + { + int prevIndex = (s.BufferIndex - 1 + HapticBufferSize) % HapticBufferSize; + int prevDxSign = s.DxSign[prevIndex]; + int prevDySign = s.DySign[prevIndex]; + + if (prevDxSign != 0 && curDxSign != 0 && prevDxSign != curDxSign) dxFlip = 1; + if (prevDySign != 0 && curDySign != 0 && prevDySign != curDySign) dyFlip = 1; + } + + // Compute current magnitude + double mag = Math.Sqrt(dx * dx + dy * dy); + + // Store signs and flip flags and magnitude in array for rolling sums + int i = s.BufferIndex; + s.DxSign[i] = curDxSign; + s.DySign[i] = curDySign; + s.DxFlipFlag[i] = dxFlip; + s.DyFlipFlag[i] = dyFlip; + s.Mag[i] = mag; + + // Increment rolling sums + if (curDxSign == 0) s.DxZeroCount++; + if (curDySign == 0) s.DyZeroCount++; + s.DxFlipCount += dxFlip; + s.DyFlipCount += dyFlip; + s.MagSum += mag; + + // Store new buffer index for later + s.BufferIndex++; + if (s.BufferIndex == HapticBufferSize) s.BufferIndex = 0; + + // No need to proceed as buffer is not full yet + if (s.BufferCount != HapticBufferSize) + return false; + + // Compute average magnitude over buffer for smoothing + double avgMag = s.MagSum / s.BufferCount; + + // Calculate a penalty for the magnitude + double bufferSpan = HapticBufferSize - HapticBufferMidpoint; + double invSpan = 1.0 / bufferSpan; + bool dxZeroEdge = s.DxZeroCount == s.BufferCount; + bool dyZeroEdge = s.DyZeroCount == s.BufferCount; + double factor = (dxZeroEdge || dyZeroEdge) ? 1.0 : 0.5; + + double dxFactor = dxZeroEdge + ? 0.0 + : (s.DxFlipCount < HapticBufferMidpoint + ? factor + : -factor * ((s.DxFlipCount - HapticBufferMidpoint) * invSpan)); + + double dyFactor = dyZeroEdge + ? 0.0 + : (s.DyFlipCount < HapticBufferMidpoint + ? factor + : -factor * ((s.DyFlipCount - HapticBufferMidpoint) * invSpan)); + + // Final penalty + s.Delta += avgMag * (dxFactor + dyFactor); + + if (s.Delta >= HapticTriggerDelta) + { + s.Delta -= Math.Floor(s.Delta / HapticTriggerDelta) * HapticTriggerDelta; + s.LastTime = now; + return true; + } + else if (s.Delta < 0) + { + s.Delta = 0; + } + } + else + { + if (!s.Cleared) + { + s.Cleared = true; + s.ResetBuffers(); + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/SteamController/Devices/MouseControllerFauxLizardMove.cs b/SteamController/Devices/MouseControllerFauxLizardMove.cs new file mode 100644 index 0000000..cf76d6f --- /dev/null +++ b/SteamController/Devices/MouseControllerFauxLizardMove.cs @@ -0,0 +1,198 @@ +namespace SteamController.Devices +{ + public partial class MouseController + { + // Adjustable parameters (tweakable settings) + private const int BufferSize = 4; // Input smoothing buffer size + private const int GestureRadius = 30; // Gesture commit threshold (pixels) + private const double GestureFlushRate = 0.1; // Rate at which gesture flush decays + private const int VelocityWindowSize = 20; // Velocity smoothing window size + private const double MinGlideMagnitude = 0.8; // Minimum velocity to start glide + private const double MinGlideVelocity = 0.15; // Minimum velocity to continue glide + + // Functional constants (derived once) + private static readonly double GestureRadiusSq = GestureRadius * GestureRadius; + private static readonly double MinGlideMagnitudeSq = MinGlideMagnitude * MinGlideMagnitude; + private static readonly double MinGlideVelocitySq = MinGlideVelocity * MinGlideVelocity; + + // Runtime state RPad (movement) + private readonly double[] bufX = new double[BufferSize]; + private int bufXIndex = 0, bufXCount = 0; + private double bufXSum = 0; + + private readonly double[] bufY = new double[BufferSize]; + private int bufYIndex = 0, bufYCount = 0; + private double bufYSum = 0; + + private double totalDeltaX = 0, totalDeltaY = 0; + private bool gestureCommitted = false; + private double gestureFlushX = 0, gestureFlushY = 0; + + private bool isGliding = false; + private bool wasTouched = false; + private long releaseTicks; + private double releaseVelocityX = 0, releaseVelocityY = 0; + + private readonly double[] velocityX = new double[VelocityWindowSize]; + private int velocityXIndex = 0, velocityXCount = 0; + private double velocityXSum = 0; + + private readonly double[] velocityY = new double[VelocityWindowSize]; + private int velocityYIndex = 0, velocityYCount = 0; + private double velocityYSum = 0; + + // Main movement logic + public void MoveByFauxLizard(double dx, double dy, bool isTouched) + { + if (!isTouched) + { + // Start glide if gesture was committed and velocity is high enough + if (!isGliding && gestureCommitted) + { + double magSq = releaseVelocityX * releaseVelocityX + releaseVelocityY * releaseVelocityY; + if (magSq >= MinGlideMagnitudeSq) + { + releaseTicks = System.Diagnostics.Stopwatch.GetTimestamp(); + isGliding = true; + } + } + + // Continue glide if active + if (isGliding) + { + double elapsed = + (System.Diagnostics.Stopwatch.GetTimestamp() - releaseTicks) + / (double)System.Diagnostics.Stopwatch.Frequency; + double glideX = ApplyReleaseGlide(releaseVelocityX, elapsed); + double glideY = ApplyReleaseGlide(releaseVelocityY, elapsed); + + double glideMagSq = glideX * glideX + glideY * glideY; + if (glideMagSq < MinGlideVelocitySq) + { + isGliding = false; + releaseVelocityX = releaseVelocityY = 0; + } + else + { + MoveBy(glideX, glideY); + } + } + + // Reset gesture state ONCE on touch release + if (wasTouched) + { + ResetFauxLizardGesture(); + wasTouched = false; + } + + return; + } + + // Touch just started + wasTouched = true; + isGliding = false; + + // Smooth input deltas + double smoothedX = SmoothRing(dx, bufX, ref bufXIndex, ref bufXCount, ref bufXSum); + double smoothedY = SmoothRing(dy, bufY, ref bufYIndex, ref bufYCount, ref bufYSum); + + // Apply acceleration ramp + double rampedX = ApplyAccelerationRamp(smoothedX); + double rampedY = ApplyAccelerationRamp(smoothedY); + + // Accumulate gesture until committed + if (!gestureCommitted) + { + totalDeltaX += rampedX; + totalDeltaY += rampedY; + + double gestureMagSq = totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY; + if (gestureMagSq > GestureRadiusSq) + { + gestureCommitted = true; + gestureFlushX = totalDeltaX; + gestureFlushY = totalDeltaY; + + velocityXIndex = velocityXCount = 0; velocityXSum = 0; + velocityYIndex = velocityYCount = 0; velocityYSum = 0; + } + else return; + } + + // Track velocity using rolling sum + releaseVelocityX = SmoothRing(rampedX, velocityX, ref velocityXIndex, ref velocityXCount, ref velocityXSum); + releaseVelocityY = SmoothRing(rampedY, velocityY, ref velocityYIndex, ref velocityYCount, ref velocityYSum); + + // Apply gesture flush + double flushedX = gestureFlushX * GestureFlushRate; + double flushedY = gestureFlushY * GestureFlushRate; + gestureFlushX -= flushedX; + gestureFlushY -= flushedY; + + // Final movement vector + double finalX = flushedX + rampedX; + double finalY = flushedY + rampedY; + + MoveBy(finalX, finalY); + } + + // Buffer smoothing helper + private static double SmoothRing(double v, double[] buffer, ref int index, ref int count, ref double sum) + { + int window = buffer.Length; + + if (count < window) + { + buffer[index] = v; + sum += v; + count++; + } + else + { + sum -= buffer[index]; + buffer[index] = v; + sum += v; + } + + index++; + if (index == window) + index = 0; + + return sum / count; + } + + // Acceleration ramp + private double ApplyAccelerationRamp(double delta) + { + double steepness = 0.1; + double midpoint = 6.0; + double maxBoost = 2.0; + double baseBoost = 1.7; + + double absDelta = Math.Abs(delta); + double sigmoidBoost = 1.0 + (maxBoost - 1.0) / (1.0 + Math.Exp(-steepness * (absDelta - midpoint))); + return delta * Math.Max(sigmoidBoost, baseBoost); + } + + // Glide decay + private double ApplyReleaseGlide(double velocity, double elapsed) + { + double rampedRate = 3.0 + Math.Pow(elapsed * 7.0, 2); + return velocity * Math.Exp(-elapsed * rampedRate); + } + + //Resets all values for a new touch + private void ResetFauxLizardGesture() + { + gestureCommitted = false; + + bufXIndex = bufXCount = 0; bufXSum = 0; + bufYIndex = bufYCount = 0; bufYSum = 0; + + velocityXIndex = velocityXCount = 0; velocityXSum = 0; + velocityYIndex = velocityYCount = 0; velocityYSum = 0; + + totalDeltaX = totalDeltaY = 0; + } + } +} \ No newline at end of file