SigmaDrift: A Biomechanical Replacement for WindMouse
WindMouse produces mouse trajectories that pass visual inspection but collapse under statistical analysis of temporal dynamics. Its gravity-and-wind model generates 15 velocity peaks where a human arm produces 1 to 3, outputs movement times uncorrelated with task geometry, and spaces samples with uniform randomness instead of the gamma distributed intervals real hardware exhibits. SigmaDrift replaces WindMouse’s heuristic force model with six interacting components drawn from computational motor control research: sigma-lognormal velocity primitives, a two phase surge architecture, Ornstein-Uhlenbeck (OU) lateral drift, Signal-Dependent Noise (SDN), speed-modulated physiological tremor, and gamma distributed timing. The result is trajectories that are statistically indistinguishable from real human input under the same feature extractors modern behavioral classifiers use.
This piece covers the WindMouse force model and why its structural properties fail under temporal analysis, the motor control foundations that SigmaDrift draws from, how those foundations converge into a single generative algorithm, head-to-head metrics against real human data, and the practical limitations of the approach.
WindMouse force model
WindMouse models cursor movement as a particle subject to two forces: gravity pulling toward the target and wind pushing in random directions. The cursor accumulates velocity from both forces each tick, gets clamped to a maximum step size, and the process repeats until the cursor lands close enough to the target.
for (int iter = 0; iter < 5000; ++iter) {
double dist = std::hypot(x1 - xs, y1 - ys);
if (dist < 1.0) break;
double w = std::min(wind_str, dist);
if (dist >= target_area) {
wx = wx / std::sqrt(3.0) + randf(-w, w) / std::sqrt(5.0);
wy = wy / std::sqrt(3.0) + randf(-w, w) / std::sqrt(5.0);
} else {
wx /= std::sqrt(3.0);
wy /= std::sqrt(3.0);
}
vx += wx + gravity * (x1 - xs) / dist;
vy += wy + gravity * (y1 - ys) / dist;
// clamp velocity, step forward, repeat
}
The gravity term is a constant attraction scaled by the inverse of remaining distance. The wind term is a random walk that decays as the cursor enters the target area. Near the target, wind dies down and gravity dominates. The algorithm is simple, fast, and the spatial paths look organic. For visual inspection and basic spatial analysis, it works. The problems appear only when examining how the cursor moves through time, not just where it ends up.
Structural failures of gravity-and-wind dynamics
WindMouse has fundamental problems that no amount of parameter tuning fixes, because they are structural to the force model itself.
No Fitts’ Law compliance. In human motor control, movement time scales logarithmically with the ratio of distance to target width. This relationship, Fitts’ Law, is one of the most replicated findings in experimental psychology. A 600px movement to a 20px target takes longer than a 600px movement to a 50px target, predictably. WindMouse movement time has no relationship to task geometry. It iterates until dist < 1.0, and how long that takes depends on random wind values, not on any model of motor planning.
Jagged velocity profile. When a human reaches for a target, the speed profile follows an asymmetric bell curve: fast acceleration, peak speed around 35% of movement time, then a longer deceleration tail. Flash and Hogan established this with their 1985 minimum jerk model. WindMouse produces a velocity profile that resembles a seismograph. The wind force creates constant random oscillations in speed, producing 10 to 20 or more velocity peaks where a real human produces 1 to 3.
Absurd sub-movement count. Each random perturbation from the wind creates a mini acceleration-deceleration cycle. Count the peaks in the speed signal above 15% of maximum and WindMouse typically produces 15 sub-movements. Real human pointing movements consist of one ballistic stroke covering most of the distance, followed by one or two small corrective adjustments. The difference is an order of magnitude.
Uniformly random timing. WindMouse adds randf(5.0, 15.0) milliseconds between each sample. Real Inter-Sample Intervals (ISI) from actual mouse hardware follow a gamma distribution: the intervals cluster around a mean with a characteristic right-skewed tail. Uniform random timing is trivial to distinguish from gamma distributed timing with a basic distribution test.
No biomechanical basis. Gravity and wind are metaphors, not motor control primitives. There is no tremor model, no signal dependent noise, no direction dependent curvature from wrist biomechanics. The algorithm has no internal model of how a human arm actually generates movement.
Any classifier examining angular velocity distributions, velocity zero-crossings, acceleration reversals, or sub-movement decomposition flags WindMouse output immediately. The spatial path might pass visual inspection, but the temporal dynamics are a dead giveaway.
Sigma-lognormal velocity primitives
Plamondon’s Kinematic Theory describes human handwriting and pointing movements as the superposition of lognormal velocity pulses, each corresponding to an agonist-antagonist muscle synergy. A single pulse produces the asymmetric bell-shaped speed profile that Flash and Hogan first characterized: a fast rise to peak velocity, then a slower deceleration tail. Path progress at time t is the Cumulative Distribution Function (CDF) of the lognormal distribution, which reduces to erf. No numerical integration required.
The mu parameter controls where peak velocity occurs. Deriving mu from the lognormal mode equation mode = exp(mu - sigma^2) and targeting peak velocity at roughly 35% of movement time produces the characteristic asymmetry observed in human reaches. Corrective sub-movements use the same primitive with different onset times, durations, and magnitudes, superimposed additively onto the running position.
Two phase surge architecture
Costello’s Surge Model (2017) decomposes human pointing into a ballistic phase and a corrective phase. The primary ballistic stroke covers 92% to 97% of the total distance on most movements. About 15% of the time, the movement overshoots to 102% to 108% instead, matching empirical data from Muller et al. (2017) on human pointing. The remaining distance gets covered by zero to two corrective sub-movements, each with its own timing parameters. This two phase structure is what produces 1 to 3 velocity peaks in real human data, not the 15 that WindMouse generates.
Ornstein-Uhlenbeck lateral drift
An OU process models the mean-reverting hand drift that occurs during movement. Unlike white noise, which accumulates without bound, the OU process has a decay term that pulls drift back toward zero. This matches the observation that hand tremor is bounded, not a random walk. The decay rate theta and diffusion coefficient sigma together determine how far the cursor wanders laterally and how quickly it recovers.
Signal dependent noise
Harris and Wolpert (1998) demonstrated that motor noise magnitude scales with the magnitude of the motor command. Faster movements require larger neural signals, which carry proportionally more noise. SDN naturally degrades endpoint accuracy in proportion to movement speed and produces realistic scatter that scales with task difficulty. A slow, careful movement to a small target has less jitter than a fast ballistic sweep.
Speed-modulated physiological tremor
Physiological tremor operates in the 8 to 12 Hz band. During fast ballistic movement, proprioceptive feedback dampens tremor gain. During the slow corrective phase and at rest, tremor expresses fully. SigmaDrift models this suppression explicitly: tremor amplitude scales inversely with instantaneous speed. No other movement generation algorithm implements this speed dependent gain modulation.
Gamma distributed sample timing
Real mouse hardware does not produce samples at perfectly uniform intervals. ISI from physical devices follow a gamma distribution, clustering around a mean with a right-skewed tail. SigmaDrift draws each sample interval from a gamma distribution parameterized to match typical 128 Hz polling behavior, replacing WindMouse’s uniform randf(5.0, 15.0) millisecond spacing.
Convergence into a single generative model
SigmaDrift combines the six foundations above into a three stage generation pipeline. None of the components act independently. They interact: tremor suppression during the ballistic phase means noise characteristics change with movement phase, SDN scales endpoint scatter with movement speed, and the OU drift creates organic wandering that constant-frequency jitter never could.
Planning. Given a start point, target point, and target width, the algorithm computes a movement plan. Movement time comes directly from Fitts’ Law (Shannon formulation) with an 8% lognormal Coefficient of Variation (CV) for trial-to-trial jitter:
MT = (a + b * log2(D/W + 1)) * exp(N(0, 0.08))
The Fitts’ compliance is prescribed, not emergent. In a fully principled biomechanical model, movement time would arise naturally from SDN dynamics: larger movements require larger motor commands, which produce more noise, which forces more correction time. Harris and Wolpert showed this in 1998. SigmaDrift has SDN, but it does not close the loop. The SDN affects endpoint scatter and trajectory jitter, but the movement time itself is computed directly from the Fitts’ equation and the trajectory is shaped to fit within that duration. The 8% CV produces realistic inter-trial variability, but the underlying timing is prescribed rather than derived from the motor control dynamics.
The primary stroke reach fraction and overshoot probability are set during planning:
bool overshoot = uniform(0.0, 1.0) < cfg.overshoot_prob;
double reach = overshoot
? uniform(cfg.overshoot_min, cfg.overshoot_max)
: uniform(cfg.undershoot_min, cfg.undershoot_max);
double primary_D = distance * reach;
Velocity profile generation. Each sub-movement follows a sigma-lognormal velocity primitive. Path progress is computed directly from the analytical CDF of the lognormal distribution. No numerical integration, no Euler stepping.
// progress along path at time t
double s = lognormal_cdf(t, 0.0, primary_mu, primary_sigma);
// position = start + direction * distance * progress
double bx = x0 + tx * primary_D * s;
double by = y0 + ty * primary_D * s;
Corrective sub-movements are additive. Each correction’s progress gets superimposed onto the running position:
for (const auto& c : corrections) {
double cs = lognormal_cdf(t, c.t0, c.mu, c.sigma);
bx += c.dir_x * c.D * cs;
by += c.dir_y * c.D * cs;
}
Noise composition. Three noise sources apply at each timestep.
OU lateral drift uses an Euler-Maruyama step:
// euler-maruyama step for OU process
ou_x += -cfg.ou_theta * ou_x * dt_s + cfg.ou_sigma * sqrt(dt_s) * N(0,1);
ou_y += -cfg.ou_theta * ou_y * dt_s + cfg.ou_sigma * sqrt(dt_s) * N(0,1);
Physiological tremor gain drops with speed:
// tremor gain drops with speed (proprioceptive suppression)
double trem_mod = 1.0 / (1.0 + speed * 0.3);
double tr_x = tremor_amp * trem_mod
* sin(2.0 * pi * tremor_freq * t_s + phase_x);
SDN scales noise with motor command magnitude:
double sdn_x = cfg.sdn_k * speed * N(0,1);
double sdn_y = cfg.sdn_k * speed * N(0,1);
The final position at each timestep is the sum of all components: the ballistic path, corrective sub-movements, curvature offset, OU drift, tremor, and signal dependent noise.
Direction dependent curvature from wrist biomechanics
Human movements in different directions have different curvature profiles because of wrist and forearm biomechanics. Horizontal movements, primarily wrist rotation, are straighter than vertical movements, primarily forearm flexion and extension. SigmaDrift models this with a perpendicular offset along the path:
curvature_amplitude = distance * 0.025 * direction_factor(angle) * N(0, 1)
The direction factor produces minimal curvature (~0.35) for horizontal movements, moderate curvature (~0.75) for diagonals, and maximum curvature (~1.15) for vertical movements. The offset follows an asymmetric profile s^2 * (1-s)^3 that peaks at 40% of the path, during the acceleration phase when the motor command is largest, not at the midpoint a Bezier curve would produce.
inline double curvature_profile(double s) {
if (s <= 0.0 || s >= 1.0) return 0.0;
double v = s * s * (1.0 - s) * (1.0 - s) * (1.0 - s);
constexpr double norm = 0.4 * 0.4 * 0.6 * 0.6 * 0.6;
return v / norm;
}
inline double direction_factor(double angle) {
double sa = std::abs(std::sin(angle));
double ca = std::abs(std::cos(angle));
return 0.5 + 0.8 * sa - 0.15 * ca;
}
Comparative metrics
Same start and end points, same distance (~630px), same target width (20px):
| Metric | SigmaDrift | WindMouse | Real human |
|---|---|---|---|
| Movement time | 827 ms | 499 ms | ~750-850 ms (Fitts’) |
| Fitts’ predicted | 764 ms | N/A | 764 ms |
| Path efficiency | 0.985 | 0.973 | 0.95-0.99 |
| Peak speed | 3.05 px/ms | 2.18 px/ms | 2.5-3.5 px/ms |
| Sub-movements | 2 | 15 | 1-3 |
| Endpoint error | 0.9 px | 0.5 px | 1-3 px |
| Velocity profile | Bell-shaped | Jagged | Bell-shaped |
| Fitts’ compliance | Yes (~8% error) | No | Yes |
Sub-movement count is the metric that matters most. WindMouse produces 15 velocity peaks because the wind force creates constant oscillations. Real humans produce 1 to 3. SigmaDrift produces 2, one ballistic and one corrective. That single metric alone gets WindMouse flagged by any competent behavioral classifier.
Movement time is the other critical difference. SigmaDrift’s 827 ms falls within 8% of the Fitts’ Law prediction for this distance and width combination, though as noted, that is because the timing is prescribed directly from the Fitts’ equation rather than emerging from the model. A detector that checks whether movement time correlates with log2(D/W + 1) across a session catches WindMouse’s 499 ms trivially, while SigmaDrift’s timing falls within the expected distribution.
WindMouse endpoint error is actually lower (0.5 px versus 0.9 px), which might seem like an advantage until you consider that real humans have 1 to 3 px of endpoint scatter. Being too accurate is its own detection vector. SigmaDrift endpoint error falls within the human range because SDN naturally degrades precision in proportion to movement speed.
Velocity profile shape
The speed graph makes the difference immediately obvious. SigmaDrift’s profile is a smooth asymmetric bell: fast rise to peak velocity at roughly 35% of movement time, then gradual deceleration with one visible corrective bump near the end. WindMouse’s profile is a noisy mess of oscillations with no discernible structure.
The bell-shaped velocity profile is the single most studied feature of human pointing movements. Flash and Hogan established this in 1985. BeCAPTCHA-Mouse’s 37-dimensional feature extractor specifically looks for sigma-lognormal decomposition of the velocity signal. An algorithm that does not produce a bell-shaped profile does not look human under analysis.
SigmaDrift generates in the same feature space that these detectors analyze, because it is built on the same motor control science they are built on. The sigma-lognormal primitives that BeCAPTCHA-Mouse uses to detect bots by decomposing trajectories are the same primitives SigmaDrift uses to generate them.
Configuration surface
The defaults work well out of the box. The parameters that matter most for tuning:
| Parameter | Default | Effect |
|---|---|---|
fitts_a / fitts_b | 50 / 150 | Movement time scaling. Calibrate to target application. |
target_width | 20.0 | Wider = faster movement, narrower = slower + more corrections |
overshoot_prob | 0.15 | 15% of movements overshoot. Raise for nervous behavior. |
curvature_scale | 0.025 | Path curvature amount. 0 = straight lines. |
ou_sigma | 1.2 | Lateral drift intensity. Higher = more visible wobble. |
tremor_amp_max | 0.55 | Hand tremor ceiling. Raise for less steady hands. |
sdn_k | 0.04 | Signal dependent noise. Higher = more jitter during fast moves. |
sample_dt_mean | 7.8 | Mean polling interval in ms. 7.8 is roughly 128 Hz. |
The most impactful parameter is target_width. Smaller targets produce longer, more careful movements with more corrections, exactly like a real human. Setting this to match the actual element size causes the algorithm’s Fitts’ Law compliance to automatically adjust movement time, correction count, and endpoint precision based on target difficulty.
motor_synergy::config cfg;
cfg.target_width = 16.0; // small button
cfg.overshoot_prob = 0.20; // slightly nervous
auto path = motor_synergy::generate(x0, y0, x1, y1, cfg);
Single header implementation
The entire algorithm lives in a single header, motor_synergy.h. C++20, zero dependencies, O(N) generation where N is the number of trajectory samples. There is no numerical integration, no iterative solvers, no neural network inference. Path progress is computed analytically from the lognormal CDF (which reduces to erf), and every noise source is a single arithmetic expression per timestep.
#include "motor_synergy.h"
auto path = motor_synergy::generate(start_x, start_y, target_x, target_y);
for (auto& pt : path) {
// pt.x, pt.y = position
// pt.t = timestamp in milliseconds
}
The included Win32 harness provides visual comparison. Hit Space for a SigmaDrift trajectory, W for WindMouse, R to record your own mouse movement. The bottom panel overlays velocity profiles for all three. Generate one of each and compare the speed graphs.
Known limitations
SigmaDrift is a parametric model, not a learned one. It does not adapt to a specific user’s movement style without manual parameter tuning. A Generative Adversarial Network (GAN) or diffusion model fitted to recorded data would be more personalized, but requires training data and inference infrastructure. SigmaDrift trades personalization for simplicity: a single header file with no dependencies.
The algorithm generates single-segment point-to-point movements. Chaining multiple segments for something like menu navigation requires calling generate() multiple times with appropriate pauses between calls. There is no built-in model for sequential movement planning.
Movement characteristics change over long sessions. Humans get fatigued, less precise, slower to react. SigmaDrift does not model fatigue. Every call to generate() produces movement with the same baseline characteristics regardless of how many movements came before it.
Fitts’ Law compliance is prescribed, not emergent. Movement time is computed directly from the Fitts’ equation with an 8% lognormal CV, then the trajectory is shaped to fit that duration. A more principled approach would have movement time arise from the SDN dynamics themselves. The SDN is already in the model, but it does not feed back into movement planning. The constants fitts_a and fitts_b are hardcoded defaults that produce reasonable timing for general pointing tasks. For best results in a specific application context, calibrate these against the actual movement times expected in that environment.
WindMouse fails under temporal feature extraction because gravity-and-wind dynamics produce the wrong velocity profile shape, the wrong sub-movement count, the wrong timing distribution, and no correlation between movement time and task geometry. SigmaDrift closes that gap by generating trajectories from the same motor control primitives, sigma-lognormal pulses, SDN, OU drift, speed-modulated tremor, that modern behavioral classifiers decompose input into, and the source is on GitHub.