How to use it
- Connect the controller and press any button; it appears in the Controller panel. Pick it from the list if more than one is connected.
- Move the sticks and watch the scopes: the trail shows the last two seconds, the “Over time” plots show each axis and both triggers for the last ten seconds.
- Set the deadzone shape, inner/outer sizes, anti-deadzone and curve to what your game or driver uses. The red dot and the response-curve graph update immediately; the settings are kept in the page link.
- Take your hands off and press Measure drift for the 3-second resting check, judged against the inner deadzone.
- Push a stick fully to the edge, press Circularity test and rotate it slowly through two full turns in 6 seconds. Read the error, coverage and min/max radius, then download the JSON or the stick image if you want to keep them.
Example
A right stick rests at X +0.300, Y 0.000. With the default scaled radial deadzone of 0.10 the processed output is (0.300 − 0.10) ÷ (1 − 0.10) = +0.222; a plain radial deadzone would pass +0.300 through unchanged, and an axial deadzone of 0.35 would silence it completely. Rolling the same stick around the rim reaches a radius of 1.00 on the axes and 1.08 on the diagonals: coverage 100%, mean error 4.1%, verdict “very round”. A controller with a square gate reaches 1.41 on the diagonals instead and is reported as over-range.
How the processing works
m = √(x² + y²) raw deflection
scaled radial: m′ = (m − inner) / (outer − inner), 0 inside, 1 beyond
radial : m′ = m when m > inner, else 0
axial : each axis rescaled on its own
curve : m″ = m′ ^ exponent (linear 1, squared 2, cubic 3)
anti-deadzone: out = anti + (1 − anti) · m″ when m″ > 0
circularity : error = mean |r_bin − 1| over the 72 directions reachedThe Gamepad API gives each axis as a value from −1 to +1 and each button as 0–1; the page reads navigator.getGamepads() every frame and redraws the canvases from those numbers. The standard mapping (17 buttons, 4 axes) lets Xbox and PlayStation names be shown; other pads get numbered buttons and consecutive axis pairs as sticks. The drift measurement is the same 3-second statistic as in the Gamepad Tester (mean offset and largest excursion). The circularity outline is the maximum radius per 5° bin; bins that never received a reading above half deflection count as uncovered.
Sources: W3C Gamepad specification – standard mapping, MDN – Gamepad API.
Everything shown is the browser’s view of the controller after the operating system and the browser’s own processing; the page does not read the controller’s internal sensors, its USB or Bluetooth report rate, or input latency, and it cannot change the controller’s calibration.
Questions
- Why do the diagonals go beyond 1.0 in the circularity test?
- Many controllers report each axis independently to ±1, so a stick pushed into the corner of a square gate reads about (1, 1) – a radius of 1.41. Round gates and drivers that clamp the vector to the unit circle give values close to 1.0 on every direction. Games normally clamp the magnitude, so a result above 1.0 is not a fault by itself; unevenness between directions is what matters.
- What deadzone and curve should I use?
- Set the inner deadzone just above the resting drift offset the drift test reports (0.05–0.15 for a healthy stick). Scaled radial keeps full range and smooth movement; axial helps when you want pure left/right or up/down input. A squared or cubic curve gives finer aiming near the centre at the cost of a slower mid-range – try each and watch the red dot.
- Does the “Over time” plot show the controller’s polling rate?
- No. The plot has one point per browser animation frame, so a 1000 Hz controller shows the same density as a 125 Hz one. Browsers do not expose the report rate; the Mouse Polling Rate Tester explains the equivalent limitation for mice.
- The visualizer stopped updating after I unplugged and reconnected the pad.
- Press a button on the reconnected controller; browsers only list a gamepad again after fresh input. The page shows a warning while a selected controller is missing and switches back to it as soon as it reappears.