Tools

Controller tester: test your gamepad for stick drift

A free controller tester that runs in your browser: check every button, trigger and stick on a PlayStation, Xbox, Switch or PC controller, and test the sticks for drift.

This free controller tester shows your controller’s buttons, triggers and sticks as you use them, checks both sticks for drift and tests the vibration motors. It runs in your web browser, so there is nothing to install. It works with PlayStation, Xbox and Switch Pro controllers, and with other USB and Bluetooth gamepads and joysticks that your browser can read.

The controller tester needs JavaScript switched on in your browser.

How to test a controller

  1. Connect it. Plug it in with a USB cable, or pair it over Bluetooth in your computer’s or phone’s settings. Some USB cables only carry power, so if nothing happens, try another cable.
  2. Press any button. Browsers keep controllers hidden from web pages until you press a button on one (in some browsers, moving a stick also works), so that sites cannot use them to help identify you. The tester appears as soon as you do.
  3. Press every button in turn. Each one lights up on the picture, and its name and number appear at the top. Squeeze the triggers slowly. On controllers with analogue triggers, such as PlayStation and Xbox ones, the red bar should fill smoothly, without jumps. On the Switch Pro Controller and Joy-Con, ZL and ZR are simple on-off buttons, so their bars jump straight to full.
  4. Move each stick. The dot should follow the stick smoothly and go back to the middle when you let go.
  5. Run the checks. Put the controller down for the drift check, roll each stick round its edge for the reach check, and try the vibration buttons.

If your browser does not recognise the controller’s layout, the tester shows every button and axis by number instead. That still tells you whether everything works.

What the results mean

The numbers. Each stick has two axes, and each axis goes from −1 to +1. On a recognised layout, −1 is fully left or fully up, and +1 is fully right or fully down. “From the centre” is how far the stick is from the middle, as a percentage of full movement. Triggers go from 0 (not pressed) to 1 (fully pressed). On a layout the browser does not recognise, the directions can differ, and triggers can show up as axes instead.

The dead zone. Few sticks rest exactly in the middle, so games ignore small movements near the centre. That circle is the dead zone, drawn dashed on each stick. If your game has a dead zone setting, set the slider to the same value. “A game sees” shows what a game with that dead zone would do with the stick: nothing inside the circle, and the rest of the movement stretched so that the edge is still full speed. Games handle dead zones in different ways, so treat it as a guide.

The drift check watches each stick for three seconds while nobody touches it, then compares the furthest it went from the centre with the dead zone:

  • No drift: it stayed well inside the dead zone, less than halfway to its edge.
  • Close to drifting: it stayed inside the dead zone, but went more than halfway to its edge. A game with a smaller dead zone may see slow movement.
  • Drift: it went outside the dead zone without being touched, so a game with that dead zone may move your character or camera on its own.
  • Moved a long way during the check: the reading went past half of the stick’s full movement. Usually that means something touched it, so put the controller on a table and check again. If it keeps happening, the stick may be badly faulty.

If the reading also wanders on its own, the tester says so. A reading that will not settle can be a sign of a worn or dirty stick.

The reach check shows how far each stick goes when you push it all the way, as a red line round its circle. If a stick reaches less than 90% in some direction, a game may never get full speed that way. Some controllers go past 100% at the diagonals, which is normal.

If a button never lights up, try another browser, and another cable or USB port, before you decide the button is broken. Some browsers and systems do not pass on the Home, Xbox or PS button, so it may not show up here.

What stick drift is

Most controller sticks measure their position with two small sensors called potentiometers: one for left and right, and one for up and down. Inside each one, a contact slides along a track, and the reading depends on where it touches. As the track and the contact wear, the reading at rest shifts, and the controller reports movement when nobody is touching the stick. That is stick drift.

When iFixit took apart Sony’s DualSense controller, it found sticks made by ALPS, whose information sheet rates them for 2,000,000 cycles. From an analysis of Call of Duty: Warzone play, iFixit worked out that this could mean about 417 hours of that kind of game. The figure varies with the games you play. Wear is not the only cause: dust and stretched springs can cause drift too.

Hall effect sticks avoid the worn track. A magnet and a magnetic sensor measure the stick’s position without touching, so iFixit says they are much less likely to drift. Some newer controllers use them, and replacement Hall effect sticks are sold for some popular controllers, though fitting one to a PlayStation or Xbox controller still means soldering.

How to fix stick drift

  1. Calibrate the sticks, if your console can. On a Nintendo Switch, go to System Settings, then Controllers and Sensors, then Calibrate Control Sticks. On a Nintendo Switch 2, go to System Settings, then Controllers & Accessories, then Calibrate Control Sticks. Nintendo recommends doing this before anything else. Then check again here.
  2. Update the controller. Consoles and controller apps can update a controller’s firmware. Nintendo includes this in its own checks for Joy-Con drift.
  3. Raise the dead zone in your game. Many games have a dead zone setting. Use the slider above to find the smallest dead zone where the drift check says “No drift”, then set your game to at least that.
  4. Take off skins and covers, and clean round the stick. Nintendo suggests removing skins and covers. If dust is part of the problem, a puff of compressed air round the base of the stick may help for a while.
  5. Replace the stick. On a Joy-Con, the stick connects with a ribbon cable, so replacing it needs no soldering: a small Phillips screwdriver, a tri-wing screwdriver, a plastic pry tool (a guitar pick will do) and tweezers. On most PlayStation and Xbox controllers the stick is soldered to the circuit board, so the old one has to be desoldered, which iFixit rates as a difficult repair. Among Sony’s and Microsoft’s own controllers, the DualSense Edge is the exception: its stick modules swap without tools, and replacement modules are sold separately.

Before you open anything, check whether the shop or the maker should fix it for you (see below). Nintendo may refuse its free repair if anyone else has opened or repaired the controller.

Your rights in the UK

Under the Consumer Rights Act 2015, goods must be of satisfactory quality, which includes being durable. Which? sums up what you can do when something goes wrong:

  • Within 30 days of getting it (for an online order, from the day it arrives), you can reject a faulty controller and get a full refund.
  • From 30 days to six months, the shop gets one chance to repair or replace it. If that fails, you can claim a refund. A fault that shows up in the first six months is taken to have been there from the start, unless the shop proves otherwise.
  • After six months, the shop still gets one chance to repair or replace it before you can claim a partial refund, but it is now up to you to show that the controller was faulty when you got it. How long a controller should last depends on things such as its price and brand.
  • The time limit for taking a claim to court is six years (five in Scotland). That is a deadline for claims, not a promise that a controller will last that long.

Your rights are against the shop that sold you the controller, not the company that made it. A screenshot of the drift check, showing the stick going outside the dead zone, can help you show the fault.

Nintendo repairs drifting Joy-Con for free. Until further notice, Nintendo does not charge for repairing Joy-Con or Nintendo Switch Lite control sticks that do not respond properly, if you bought them in the UK, the EEA or Switzerland. This applies even when the cause is wear and tear, and even after Nintendo’s 24-month warranty has ended. Nintendo may refuse if the controller has been opened, modified or repaired by anyone else, damaged, used for rental or business, or affected by accessories not made by or for Nintendo, or if its serial number has been tampered with. Its page for the newer Joy-Con 2 controllers leads to its repair form but does not say whether that repair is free, so ask when you book.

Sony and Microsoft also give their controllers a manufacturer’s warranty; check the terms on their support websites. A manufacturer’s warranty comes on top of your rights against the shop, not instead of them.

Which controllers work

The tester reads controllers through your browser, so it can see any controller your browser can. When the browser recognises the layout, as Chrome and Edge do for PlayStation, Xbox and Switch Pro controllers among others, the tester draws the controller and names the buttons: Cross, Circle, Square and Triangle on a PlayStation controller, B, A, Y and X on a Nintendo one, and A, B, X and Y on an Xbox one. Other recognised controllers get names that say where each button is.

If the layout is not recognised, the tester lists every button and axis by number and draws the first two pairs of axes as sticks. A d-pad sometimes shows up as a single axis that jumps between values, rather than as four buttons.

Arcade sticks, flight sticks and retro-style USB pads show up in the same way. Old 9-pin joysticks from Atari, Commodore 64 and Amiga computers need a USB adapter before a computer can see them; with one, they usually show up as two axes for the directions and a button for fire. For retro games without a joystick, our Commodore 64 emulator can turn your keyboard into one: W, A, S and D to move and the left Shift key to fire. And if you collect the real thing, see our list of NES games released in the UK and Europe.

The tester also works on phones and tablets, with a controller paired over Bluetooth. Vibration needs a browser that can drive the motors. Chrome and Edge can drive many controllers’ motors, and so can Safari on a Mac, but Firefox cannot, and neither can browsers on iPhones and iPads. Even in a browser that offers vibration, some controllers stay still over some connections, so if you feel nothing, see the questions below before you decide the motors are broken.

Questions people ask

Does the tester install anything or send my readings anywhere?

No. It is a small program that runs in your browser while this page is open. It reads your controller through the browser and does not send the readings anywhere.

Why doesn’t my controller show up?

Press a button on it first: browsers only show a controller to a page after you press a button on it (or, in some browsers, move a stick). If it still does not appear, check that your computer or phone can see the controller in its own settings, try a USB cable that carries data rather than just power, and close any app that may be taking over the controller. A wireless controller may have gone to sleep, so press its Home, Xbox or PS button to wake it.

Why doesn’t my controller vibrate here?

The browser has to be able to drive that controller’s motors, and it cannot always do so, especially over Bluetooth. If the tester says your browser cannot make the controller vibrate, or you feel nothing when you press the vibration buttons, connect the controller with a USB cable and try again, then try another browser (on a Mac, try Safari as well as Chrome). If the controller vibrates in a game but not here, the motors are fine and the browser is the problem. On Xbox One and Series controllers, the small motors in the triggers can only be tested where the browser supports them, which is why the Trigger motors button does not always appear.

How much drift is normal?

Almost every stick reads a little off centre at rest, which is why games have a dead zone at all. Microsoft’s XInput guide for Windows games uses default dead zones of 7,849 and 8,689 on a scale that goes up to 32,767: about 24% of the movement for an Xbox controller’s left stick and 27% for the right. It also notes that the dead zone needed varies from controller to controller. What matters is whether your stick stays inside the dead zone your game uses, and that is what the drift check tells you.

Can I use this tester on my own website?

Yes. The full code is below. It is a single block of HTML and JavaScript with no other files, so you can paste it into a WordPress Custom HTML block or any web page. The page must have an address starting https://, because browsers only let secure pages read controllers.

More free tools that run in your browser are on our free tools page.

Sources

MDN: Using the Gamepad API; MDN: GamepadHapticActuator playEffect() (vibration); W3C: Gamepad; Microsoft: Getting started with XInput and XINPUT_GAMEPAD; VGC: PS5 drift investigation suggests DualSense sticks “have a life of 417 hours” (iFixit’s findings); iFixit: Hall-effect joysticks; Nintendo UK: How to calibrate the controllers and on Nintendo Switch 2; Nintendo UK: Joy-Con control sticks (free repair); Nintendo UK: Nintendo Switch Lite control sticks (free repair); Nintendo UK: Joy-Con 2 control sticks; Nintendo Life: how to fix Joy-Con drift (replacing the stick); iFixit: DualSense joystick replacement; PlayStation: Replace a stick module on your controller (DualSense Edge); Which?: Consumer Rights Act 2015.

Use this tester on your own site

You are welcome to use this tester on your own website. Copy the code below into a Custom HTML block in WordPress, or into any web page with an address starting https:// (browsers only let secure pages read controllers). It needs no other files. We only ask that you keep the link to Braindumps at the bottom.

<!-- Braindumps controller tester: gamepads, joysticks and joypads in the browser, with a stick drift check. Self-contained: no external files. -->
<div id="bd-pad">
<style>
#bd-pad{--bd-red:#d63333;--bd-line:rgba(127,127,127,.35);--bd-soft:rgba(127,127,127,.08);--bd-ok:#1f9d55;--bd-warn:#c77d00;font:inherit;color:inherit;max-width:760px;margin:1.5em 0}
#bd-pad *{box-sizing:border-box}
#bd-pad [hidden]{display:none!important}
#bd-pad p{margin:0 0 8px}
#bd-pad .bd-pad-wait{border:2px dashed var(--bd-line);border-radius:10px;padding:22px 16px;text-align:center}
#bd-pad .bd-pad-wait svg{width:84px;height:auto;opacity:.75;margin-bottom:6px}
#bd-pad .bd-pad-big{font-size:1.2em;font-weight:700}
#bd-pad .bd-pad-small{font-size:.9em;opacity:.85}
#bd-pad .bd-pad-head{border:2px solid var(--bd-red);border-radius:10px;padding:10px 14px;background:var(--bd-soft);margin-bottom:12px}
#bd-pad .bd-pad-name{font-weight:700;font-size:1.1em;margin:0 0 2px;overflow-wrap:anywhere}
#bd-pad .bd-pad-meta{font-size:.88em;opacity:.85;margin:0}
#bd-pad .bd-pad-tabs{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px}
#bd-pad button{font:inherit;font-size:.95em;color:inherit;background:transparent;border:1px solid var(--bd-line);border-radius:999px;padding:7px 14px;cursor:pointer;line-height:1.3}
#bd-pad button:hover,#bd-pad button:focus-visible{border-color:var(--bd-red)}
#bd-pad button[aria-selected="true"],#bd-pad button.bd-pad-go{border-color:var(--bd-red);background:var(--bd-soft);font-weight:700}
#bd-pad button:disabled{opacity:.5;cursor:default}
#bd-pad .bd-pad-svg{width:100%;max-width:560px;display:block;margin:0 auto 6px}
#bd-pad .bd-pad-svg .shape{fill:var(--bd-soft);stroke:currentColor;stroke-width:2;opacity:.9}
#bd-pad .bd-pad-svg .btn{fill:transparent;stroke:currentColor;stroke-width:2}
#bd-pad .bd-pad-svg .btn.on{fill:var(--bd-red);stroke:var(--bd-red)}
#bd-pad .bd-pad-svg .btn.trig.on{fill:transparent}
#bd-pad .bd-pad-svg .bar{fill:var(--bd-red);opacity:.85}
#bd-pad .bd-pad-svg text{fill:currentColor;font:600 13px/1 system-ui,sans-serif;text-anchor:middle;dominant-baseline:central;pointer-events:none}
#bd-pad .bd-pad-svg .btn.on+text{fill:#fff}
#bd-pad .bd-pad-svg .knob{fill:currentColor;opacity:.8}
#bd-pad .bd-pad-svg .knob.on{fill:#fff;opacity:1}
#bd-pad .bd-pad-sticks{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:10px 0 14px}
#bd-pad .bd-pad-stick{border:1px solid var(--bd-line);border-radius:10px;padding:10px;text-align:center}
#bd-pad .bd-pad-stick canvas{width:100%;max-width:220px;aspect-ratio:1;display:block;margin:0 auto 6px}
#bd-pad .bd-pad-stick p{font-size:.88em;margin:0;font-variant-numeric:tabular-nums}
#bd-pad .bd-pad-stick b{font-size:1.05em}
#bd-pad .bd-pad-stick span{white-space:nowrap}
#bd-pad fieldset{border:1px solid var(--bd-line);border-radius:10px;padding:12px 16px 12px;margin:0 0 14px;min-width:0}
#bd-pad legend{font-weight:700;padding:0 6px}
#bd-pad .bd-pad-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:8px 0}
#bd-pad label.bd-pad-dzl{display:flex;flex-wrap:wrap;align-items:center;gap:10px;font-size:.95em;margin:4px 0 8px}
#bd-pad input[type=range]{flex:1 1 120px;min-width:0;accent-color:var(--bd-red)}
#bd-pad output{font-weight:700;min-width:3.5em;font-variant-numeric:tabular-nums}
#bd-pad .bd-pad-res{margin-top:8px}
#bd-pad .bd-pad-res:empty{display:none}
#bd-pad .bd-pad-v{border-left:4px solid var(--bd-line);padding:6px 10px;margin:8px 0;background:var(--bd-soft);border-radius:0 6px 6px 0}
#bd-pad .bd-pad-v.ok{border-left-color:var(--bd-ok)}
#bd-pad .bd-pad-v.warn{border-left-color:var(--bd-warn)}
#bd-pad .bd-pad-v.bad{border-left-color:var(--bd-red)}
#bd-pad .bd-pad-v strong{display:block}
#bd-pad details{margin:0 0 14px;border:1px solid var(--bd-line);border-radius:10px;padding:8px 14px}
#bd-pad summary{cursor:pointer;font-weight:700}
#bd-pad .bd-pad-raw{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:6px 12px;margin-top:10px;font-size:.85em;font-variant-numeric:tabular-nums}
#bd-pad .bd-pad-item{display:grid;grid-template-columns:1fr auto;gap:2px 8px;align-items:center;padding:4px 6px;border-radius:6px}
#bd-pad .bd-pad-item.on{background:var(--bd-soft);outline:1px solid var(--bd-red)}
#bd-pad .bd-pad-item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#bd-pad .bd-pad-meter{grid-column:1 / -1;height:6px;border-radius:3px;background:var(--bd-line);position:relative;overflow:hidden}
#bd-pad .bd-pad-meter i{position:absolute;top:0;bottom:0;left:0;background:var(--bd-red);width:0}
#bd-pad .bd-pad-meter.axis i{left:50%}
#bd-pad .bd-pad-meter.axis::after{content:"";position:absolute;left:50%;top:0;bottom:0;width:1px;background:currentColor;opacity:.5}
@media (max-width:560px){#bd-pad .bd-pad-sticks{gap:8px}#bd-pad .bd-pad-stick{padding:8px 4px}#bd-pad .bd-pad-stick p{font-size:.78em}}
@media (max-width:330px){#bd-pad .bd-pad-sticks{grid-template-columns:1fr}}
</style>

<div class="bd-pad-wait" id="bd-pad-wait" aria-live="polite">
<svg viewBox="0 0 120 70" aria-hidden="true"><path d="M30 10h60c12 0 20 10 24 26s2 28-8 28c-8 0-12-10-20-12H38c-8 2-12 12-20 12-10 0-12-12-8-28S18 10 30 10z" fill="none" stroke="currentColor" stroke-width="3"/><circle cx="36" cy="30" r="6" fill="none" stroke="currentColor" stroke-width="3"/><circle cx="84" cy="24" r="3" fill="currentColor"/><circle cx="92" cy="32" r="3" fill="currentColor"/><circle cx="76" cy="32" r="3" fill="currentColor"/><circle cx="84" cy="40" r="3" fill="currentColor"/></svg>
<p class="bd-pad-big" id="bd-pad-wmsg">Connect a controller, then press any button on it.</p>
<p class="bd-pad-small">Plug it in with a USB cable or pair it over Bluetooth first. Your browser only lets a page see a controller after you press one of its buttons.</p>
</div>

<div id="bd-pad-main" hidden>
<div class="bd-pad-tabs" id="bd-pad-tabs" role="tablist" aria-label="Connected controllers" hidden></div>
<div class="bd-pad-head">
<p class="bd-pad-name" id="bd-pad-name"></p>
<p class="bd-pad-meta" id="bd-pad-meta"></p>
<p class="bd-pad-meta" id="bd-pad-last" aria-live="polite">Press a button to see its name and number.</p>
</div>

<svg class="bd-pad-svg" id="bd-pad-svg" viewBox="0 0 420 260" role="img" aria-label="Picture of the controller: buttons light up as you press them">
<g id="bd-pad-trig">
<rect class="btn trig" x="80" y="6" width="62" height="24" rx="7" data-b="6"/><rect class="bar" x="80" y="6" width="0" height="24" rx="7" data-bar="6"/><text x="111" y="18" data-l="6"></text>
<rect class="btn trig" x="278" y="6" width="62" height="24" rx="7" data-b="7"/><rect class="bar" x="278" y="6" width="0" height="24" rx="7" data-bar="7"/><text x="309" y="18" data-l="7"></text>
</g>
<rect class="btn" x="72" y="36" width="80" height="16" rx="8" data-b="4"/><text x="112" y="44" data-l="4"></text>
<rect class="btn" x="268" y="36" width="80" height="16" rx="8" data-b="5"/><text x="308" y="44" data-l="5"></text>
<path class="shape" d="M92 60c30-10 70-6 118-6s88-4 118 6c40 14 66 82 72 128 6 44-24 64-50 46-20-14-34-30-58-34H128c-24 4-38 20-58 34-26 18-56-2-50-46 6-46 32-114 72-128z"/>
<g id="bd-pad-ls"><circle class="btn" cx="128" cy="104" r="28" data-b="10"/><circle class="knob" cx="128" cy="104" r="15" data-k="0"/></g>
<circle class="btn" cx="262" cy="160" r="28" data-b="11"/><circle class="knob" cx="262" cy="160" r="15" data-k="1"/>
<g id="bd-pad-dpad">
<rect class="btn" x="146" y="132" width="20" height="22" rx="4" data-b="12"/>
<rect class="btn" x="146" y="166" width="20" height="22" rx="4" data-b="13"/>
<rect class="btn" x="122" y="150" width="22" height="20" rx="4" data-b="14"/>
<rect class="btn" x="168" y="150" width="22" height="20" rx="4" data-b="15"/>
</g>
<circle class="btn" cx="308" cy="128" r="14" data-b="0"/><text x="308" y="128" data-l="f0"></text>
<circle class="btn" cx="334" cy="102" r="14" data-b="1"/><text x="334" y="102" data-l="f1"></text>
<circle class="btn" cx="282" cy="102" r="14" data-b="2"/><text x="282" y="102" data-l="f2"></text>
<circle class="btn" cx="308" cy="76" r="14" data-b="3"/><text x="308" y="76" data-l="f3"></text>
<rect class="btn" x="170" y="92" width="26" height="14" rx="7" data-b="8"/>
<rect class="btn" x="224" y="92" width="26" height="14" rx="7" data-b="9"/>
<circle class="btn" cx="210" cy="128" r="11" data-b="16"/>
<rect class="btn" x="180" y="64" width="60" height="18" rx="6" data-b="17" hidden/>
</svg>

<div class="bd-pad-sticks" id="bd-pad-sticks">
<div class="bd-pad-stick"><canvas id="bd-pad-c0" width="440" height="440" aria-hidden="true"></canvas><p><b id="bd-pad-t0">Left stick</b><br>x <span id="bd-pad-x0">0.000</span> · y <span id="bd-pad-y0">0.000</span><br>From the centre: <span id="bd-pad-r0">0.0%</span><br>A game sees: <span id="bd-pad-g0">no movement</span></p></div>
<div class="bd-pad-stick"><canvas id="bd-pad-c1" width="440" height="440" aria-hidden="true"></canvas><p><b id="bd-pad-t1">Right stick</b><br>x <span id="bd-pad-x1">0.000</span> · y <span id="bd-pad-y1">0.000</span><br>From the centre: <span id="bd-pad-r1">0.0%</span><br>A game sees: <span id="bd-pad-g1">no movement</span></p></div>
</div>

<fieldset>
<legend>Stick drift check</legend>
<label class="bd-pad-dzl">Dead zone <input type="range" id="bd-pad-dz" min="1" max="30" step="1" value="10" aria-describedby="bd-pad-dzhelp"><output id="bd-pad-dzv" for="bd-pad-dz">10%</output></label>
<p class="bd-pad-small" id="bd-pad-dzhelp">The dead zone is the circle round the centre where a game ignores the stick (drawn dashed above). If your game has a dead zone setting, set the same value here. Microsoft's guide for Windows games uses about 24% for an Xbox controller's left stick and 27% for the right.</p>
<div class="bd-pad-row"><button type="button" class="bd-pad-go" id="bd-pad-dgo">Check for drift</button><span class="bd-pad-small" id="bd-pad-dmsg">Let go of both sticks first. The check takes 3 seconds.</span></div>
<div class="bd-pad-res" id="bd-pad-dres" aria-live="polite"></div>
</fieldset>

<fieldset>
<legend>Reach check</legend>
<p class="bd-pad-small">Press Start, then roll each stick slowly round its outer edge two or three times, and press Finish. It shows whether each stick reaches full movement in every direction.</p>
<div class="bd-pad-row"><button type="button" class="bd-pad-go" id="bd-pad-rgo">Start</button><span class="bd-pad-small" id="bd-pad-rmsg"></span></div>
<div class="bd-pad-res" id="bd-pad-rres" aria-live="polite"></div>
</fieldset>

<fieldset>
<legend>Vibration</legend>
<div class="bd-pad-row" id="bd-pad-vrow"><button type="button" data-m="weak">Light motor</button><button type="button" data-m="strong">Heavy motor</button><button type="button" data-m="both">Both motors</button><button type="button" data-m="triggers" id="bd-pad-vtrig" hidden>Trigger motors</button></div>
<p class="bd-pad-small" id="bd-pad-vmsg" aria-live="polite"></p>
</fieldset>

<details id="bd-pad-rawbox">
<summary>Every button and axis, as numbers</summary>
<div class="bd-pad-raw" id="bd-pad-raw"></div>
</details>
</div>

<p class="bd-credit" style="font-size:.85em;opacity:.75;margin:10px 0 0">Controller tester by <a href="https://braindumps.co.uk/controller-tester/">Braindumps.co.uk</a></p>
<script>
(function () {
  "use strict";
  /* BEGIN CALC */
  // Browsers describe a controller in different ways. Chrome and Edge write
  // "Name (STANDARD GAMEPAD Vendor: 045e Product: 0b13)", or on Windows
  // "Xbox 360 Controller (XInput STANDARD GAMEPAD)". Firefox writes "45e-b13-Name".
  // Safari gives just the name.
  function hex4(h) { return h ? ("0000" + h.toLowerCase()).slice(-4) : ""; }
  function parseId(id) {
    var s = String(id || "").trim(), vendor = "", product = "", m;
    m = s.match(/Vendor:\s*([0-9a-f]{1,4})\s+Product:\s*([0-9a-f]{1,4})/i);
    if (m) { vendor = m[1]; product = m[2]; }
    else {
      m = s.match(/^([0-9a-f]{1,4})-([0-9a-f]{1,4})-/i);
      if (m) { vendor = m[1]; product = m[2]; }
    }
    var name = s.replace(/\s*\([^()]*(?:Vendor:|STANDARD GAMEPAD)[^()]*\)\s*$/i, "").replace(/^[0-9a-f]{1,4}-[0-9a-f]{1,4}-/i, "").trim();
    if (/^xinput$/i.test(name)) { name = "XInput controller"; }
    return { name: name || "Controller", vendor: hex4(vendor), product: hex4(product) };
  }
  // Which company made it, for the button names. Unknown makers get names that describe
  // where each button is.
  function brandOf(vendor, name) {
    var n = String(name || "").toLowerCase();
    if (vendor === "054c" || /dualsense|dualshock|playstation/.test(n)) { return "sony"; }
    if (vendor === "045e" || /xbox|x-box|xinput/.test(n)) { return "microsoft"; }
    if (vendor === "057e" || /nintendo|joy-con|^pro controller$/.test(n)) { return "nintendo"; }
    return "generic";
  }
  var NAMES = {
    generic: ["Bottom button", "Right button", "Left button", "Top button", "Left shoulder", "Right shoulder", "Left trigger", "Right trigger", "Select", "Start", "Left stick press", "Right stick press", "D-pad up", "D-pad down", "D-pad left", "D-pad right", "Home"],
    microsoft: ["A", "B", "X", "Y", "LB", "RB", "LT", "RT", "View or Back", "Menu or Start", "Left stick press", "Right stick press", "D-pad up", "D-pad down", "D-pad left", "D-pad right", "Xbox button"],
    sony: ["Cross", "Circle", "Square", "Triangle", "L1", "R1", "L2", "R2", "Create or Share", "Options", "L3", "R3", "D-pad up", "D-pad down", "D-pad left", "D-pad right", "PS button", "Touchpad press"],
    nintendo: ["B", "A", "Y", "X", "L", "R", "ZL", "ZR", "Minus", "Plus", "Left stick press", "Right stick press", "D-pad up", "D-pad down", "D-pad left", "D-pad right", "Home"]
  };
  var FACE = { generic: ["", "", "", ""], microsoft: ["A", "B", "X", "Y"], sony: ["✕", "○", "□", "△"], nintendo: ["B", "A", "Y", "X"] };
  var SHOULDER = { generic: ["L1", "R1", "L2", "R2"], microsoft: ["LB", "RB", "LT", "RT"], sony: ["L1", "R1", "L2", "R2"], nintendo: ["L", "R", "ZL", "ZR"] };
  function buttonName(brand, i, standard) {
    var list = NAMES[brand] || NAMES.generic;
    return standard && list[i] ? list[i] : "Button " + i;
  }
  // Where the stick sits while nobody touches it, from samples taken over a few seconds.
  function driftStats(samples) {
    var n = samples.length, sx = 0, sy = 0, maxR = 0, spread = 0, i, r;
    if (!n) { return { n: 0, mx: 0, my: 0, offset: 0, maxR: 0, spread: 0 }; }
    for (i = 0; i < n; i++) {
      sx += samples[i][0]; sy += samples[i][1];
      r = Math.sqrt(samples[i][0] * samples[i][0] + samples[i][1] * samples[i][1]);
      if (r > maxR) { maxR = r; }
    }
    var mx = sx / n, my = sy / n;
    for (i = 0; i < n; i++) {
      r = Math.sqrt((samples[i][0] - mx) * (samples[i][0] - mx) + (samples[i][1] - my) * (samples[i][1] - my));
      if (r > spread) { spread = r; }
    }
    return { n: n, mx: mx, my: my, offset: Math.sqrt(mx * mx + my * my), maxR: maxR, spread: spread };
  }
  // The verdict compares the furthest the stick went from the centre while untouched with the
  // dead zone: anything outside the dead zone reaches the game as movement.
  function driftVerdict(st, dz) {
    if (!st || !st.n) { return "none"; }
    if (st.maxR > 0.5) { return "moved"; }
    var okLimit = dz > 0 ? dz / 2 : 0.02, driftLimit = dz > 0 ? dz : 0.05;
    if (st.maxR < okLimit) { return "ok"; }
    if (st.maxR < driftLimit) { return "borderline"; }
    return "drift";
  }
  // What a game with a round dead zone does with a stick position: nothing inside the dead zone,
  // and the rest of the travel stretched so the edge still means full speed.
  function withDeadZone(x, y, dz) {
    var r = Math.sqrt(x * x + y * y);
    if (r <= dz || r === 0) { return [0, 0]; }
    var k = Math.min(1, (r - dz) / (1 - dz)) / r;
    return [x * k, y * k];
  }
  // The furthest each stick reached in each of 36 directions while it was rolled round its edge.
  function reachStats(points, bins) {
    bins = bins || 36;
    var best = [], seen = [], i, b, a, r;
    for (i = 0; i < bins; i++) { best.push(0); seen.push(false); }
    for (i = 0; i < points.length; i++) {
      r = Math.sqrt(points[i][0] * points[i][0] + points[i][1] * points[i][1]);
      if (r < 0.5) { continue; }
      a = Math.atan2(-points[i][1], points[i][0]);
      if (a < 0) { a += 2 * Math.PI; }
      b = Math.floor(a / (2 * Math.PI) * bins) % bins;
      seen[b] = true;
      if (r > best[b]) { best[b] = r; }
    }
    var covered = 0, min = Infinity, minBin = -1, sum = 0;
    for (b = 0; b < bins; b++) {
      if (!seen[b]) { continue; }
      covered++; sum += best[b];
      if (best[b] < min) { min = best[b]; minBin = b; }
    }
    return { bins: best, seen: seen, covered: covered, mean: covered ? sum / covered : 0, min: covered ? min : 0, minAngle: minBin < 0 ? null : (minBin + 0.5) * 360 / bins };
  }
  function reachVerdict(rs) {
    if (!rs || rs.covered < rs.bins.length * 0.8) { return "incomplete"; }
    return rs.min < 0.9 ? "short" : "ok";
  }
  // Angles count anticlockwise from "right", with "up" at 90 degrees.
  function dirName(angle) {
    var names = ["right", "up and right", "up", "up and left", "left", "down and left", "down", "down and right"];
    return names[Math.round(((angle % 360) + 360) % 360 / 45) % 8];
  }
  function pct(v) { return (Math.round(v * 1000) / 10).toFixed(1) + "%"; }
  /* END CALC */

  var root = document.getElementById("bd-pad");
  if (!root) { return; }
  function el(id) { return document.getElementById(id); }
  // SVG elements have no hidden property, so set the attribute (the style hides anything with it).
  function hide(node, yes) { if (yes) { node.setAttribute("hidden", ""); } else { node.removeAttribute("hidden"); } }
  var wait = el("bd-pad-wait"), main = el("bd-pad-main"), svg = el("bd-pad-svg");
  var raw = el("bd-pad-raw"), tabs = el("bd-pad-tabs");
  // Browsers only let secure pages (https:// addresses) read controllers.
  if (window.isSecureContext === false) {
    el("bd-pad-wmsg").textContent = "Browsers only let secure pages, with addresses starting https://, read controllers, so the tester cannot work on this page.";
    return;
  }
  var supported = typeof navigator.getGamepads === "function";
  if (!supported) {
    el("bd-pad-wmsg").textContent = "This browser cannot read controllers. Try Chrome, Edge, Firefox or Safari.";
    return;
  }

  var current = null;     // index of the controller being shown
  var shownKey = "";      // what the page was last built for (index, id and layout)
  var brand = "generic", standard = true;
  var trails = [[], []];
  var reach = null;       // while the reach check runs: points for each stick
  var reachDone = [null, null];
  var drift = null;       // while the drift check runs
  var lastPressed = [];

  function pads() {
    var list = [], all;
    try { all = navigator.getGamepads(); } catch (e) { all = []; }
    for (var i = 0; all && i < all.length; i++) { if (all[i] && all[i].connected !== false) { list.push(all[i]); } }
    return list;
  }
  function deadZone() { return Number(el("bd-pad-dz").value) / 100; }
  function ink() { return getComputedStyle(root).color || "#888"; }
  function stickAxes() { return [[0, 1], [2, 3]]; }

  // Rebuild the parts of the page that depend on which controller is shown.
  function setup(gp) {
    var info = parseId(gp.id);
    standard = gp.mapping === "standard";
    brand = brandOf(info.vendor, info.name);
    el("bd-pad-name").textContent = info.name;
    var bits = [standard ? "Layout recognised by your browser" : "Layout not recognised: buttons and axes are shown by number"];
    bits.push(gp.buttons.length + " buttons", gp.axes.length + " axes");
    if (info.vendor) { bits.push("vendor " + info.vendor + ", product " + info.product); }
    bits.push(canRumble(gp) ? "your browser offers vibration" : "no vibration in this browser");
    el("bd-pad-meta").textContent = bits.join(" · ");
    hide(svg, !standard);
    var face = FACE[brand], sh = SHOULDER[brand];
    svg.querySelectorAll("[data-l]").forEach(function (t) {
      var k = t.getAttribute("data-l");
      t.textContent = k.charAt(0) === "f" ? face[Number(k.slice(1))] : sh[[4, 5, 6, 7].indexOf(Number(k))] || "";
    });
    var b17 = svg.querySelector('[data-b="17"]');
    if (b17) { hide(b17, !(standard && gp.buttons.length > 17)); }
    // PlayStation pads have the d-pad at the top left and both sticks low down; the others have
    // the left stick at the top left.
    [["bd-pad-ls", "translate(30 56)"], ["bd-pad-dpad", "translate(-28 -56)"]].forEach(function (g) {
      if (brand === "sony") { el(g[0]).setAttribute("transform", g[1]); } else { el(g[0]).removeAttribute("transform"); }
    });
    el("bd-pad-t0").textContent = standard ? "Left stick" : "Axes 0 and 1";
    el("bd-pad-t1").textContent = standard ? "Right stick" : "Axes 2 and 3";
    el("bd-pad-sticks").children[1].hidden = gp.axes.length < 4;
    el("bd-pad-sticks").hidden = gp.axes.length < 2;
    var html = "";
    gp.buttons.forEach(function (b, i) {
      html += '<div class="bd-pad-item" data-rb="' + i + '"><span>' + i + ": " + buttonName(brand, i, standard) + '</span><em>0.00</em><div class="bd-pad-meter"><i></i></div></div>';
    });
    gp.axes.forEach(function (a, i) {
      var label = standard && i < 4 ? ["Left stick x", "Left stick y", "Right stick x", "Right stick y"][i] : "Axis " + i;
      html += '<div class="bd-pad-item" data-ra="' + i + '"><span>' + label + '</span><em>0.000</em><div class="bd-pad-meter axis"><i></i></div></div>';
    });
    raw.innerHTML = html;
    el("bd-pad-rawbox").open = !standard;
    trails = [[], []];
    reachDone = [null, null];
    el("bd-pad-dres").innerHTML = "";
    el("bd-pad-rres").innerHTML = "";
    el("bd-pad-vmsg").textContent = canRumble(gp) ? "" : "Your browser cannot make this controller vibrate. Try a USB cable, or another browser: Chrome and Edge can drive many controllers' motors, and so can Safari on a Mac.";
    el("bd-pad-vrow").hidden = !canRumble(gp);
    el("bd-pad-vtrig").hidden = !canEffect(gp, "trigger-rumble");
    el("bd-pad-last").textContent = "Press a button to see its name and number.";
    lastPressed = [];
  }
  // Whether the browser offers a vibration effect for this controller. Newer browsers list the
  // effects each controller supports; older ones only have the two main motors ("dual-rumble").
  function canEffect(gp, type) {
    var va = gp.vibrationActuator;
    if (!va || typeof va.playEffect !== "function") { return false; }
    if (va.effects && typeof va.effects.indexOf === "function") { return va.effects.indexOf(type) !== -1; }
    return type === "dual-rumble";
  }
  function canRumble(gp) { return canEffect(gp, "dual-rumble"); }

  function drawTabs(list) {
    tabs.hidden = list.length < 2;
    var html = "";
    list.forEach(function (gp) {
      html += '<button type="button" role="tab" data-i="' + gp.index + '" aria-selected="' + (gp.index === current) + '">' + (gp.index + 1) + ": " + escapeHtml(parseId(gp.id).name) + "</button>";
    });
    if (tabs.getAttribute("data-html") !== html) { tabs.innerHTML = html; tabs.setAttribute("data-html", html); }
  }
  function escapeHtml(s) { return String(s).replace(/[&<>"]/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]; }); }
  tabs.addEventListener("click", function (e) {
    var b = e.target.closest("button[data-i]");
    if (b) { current = Number(b.getAttribute("data-i")); shownKey = ""; }
  });

  function drawStick(n, x, y) {
    var c = el("bd-pad-c" + n);
    if (!c || c.parentNode.hidden) { return; }
    var g = c.getContext("2d"), S = c.width, mid = S / 2, R = S * 0.42, col = ink(), dz = deadZone();
    g.clearRect(0, 0, S, S);
    g.lineWidth = 2; g.strokeStyle = col; g.fillStyle = col;
    g.globalAlpha = 0.25;
    g.beginPath(); g.moveTo(mid - R, mid); g.lineTo(mid + R, mid); g.moveTo(mid, mid - R); g.lineTo(mid, mid + R); g.stroke();
    g.globalAlpha = 0.8;
    g.beginPath(); g.arc(mid, mid, R, 0, 2 * Math.PI); g.stroke();
    if (dz > 0) {
      g.globalAlpha = 0.12; g.fillStyle = "#d63333";
      g.beginPath(); g.arc(mid, mid, R * dz, 0, 2 * Math.PI); g.fill();
      g.globalAlpha = 0.9; g.setLineDash([8, 8]); g.strokeStyle = "#d63333";
      g.beginPath(); g.arc(mid, mid, R * dz, 0, 2 * Math.PI); g.stroke();
      g.setLineDash([]);
    }
    var rs = reachDone[n];
    if (rs) {
      g.globalAlpha = 0.9; g.strokeStyle = "#d63333"; g.lineWidth = 3;
      g.beginPath();
      var first = true;
      for (var b = 0; b < rs.bins.length; b++) {
        if (!rs.seen[b]) { continue; }
        var a = (b + 0.5) / rs.bins.length * 2 * Math.PI, r = Math.min(rs.bins[b], 1.45);
        var px = mid + Math.cos(a) * r * R, py = mid - Math.sin(a) * r * R;
        if (first) { g.moveTo(px, py); first = false; } else { g.lineTo(px, py); }
      }
      if (!first) { g.closePath(); g.stroke(); }
      g.lineWidth = 2;
    }
    var t = trails[n];
    t.push([x, y]);
    if (t.length > 40) { t.shift(); }
    g.fillStyle = col;
    for (var i = 0; i < t.length; i++) {
      g.globalAlpha = 0.04 + 0.4 * i / t.length;
      g.beginPath(); g.arc(mid + t[i][0] * R, mid + t[i][1] * R, 5, 0, 2 * Math.PI); g.fill();
    }
    var outside = Math.sqrt(x * x + y * y) > dz;
    g.globalAlpha = 1; g.fillStyle = outside ? "#d63333" : col;
    g.beginPath(); g.arc(mid + Math.max(-1.45, Math.min(1.45, x)) * R, mid + Math.max(-1.45, Math.min(1.45, y)) * R, 13, 0, 2 * Math.PI); g.fill();
  }

  function showStick(n, gp) {
    var ax = stickAxes()[n];
    if (gp.axes.length <= ax[1]) { return; }
    var x = gp.axes[ax[0]], y = gp.axes[ax[1]], r = Math.sqrt(x * x + y * y);
    el("bd-pad-x" + n).textContent = (x >= 0 ? "+" : "") + x.toFixed(3);
    el("bd-pad-y" + n).textContent = (y >= 0 ? "+" : "") + y.toFixed(3);
    el("bd-pad-r" + n).textContent = pct(r);
    var seen = withDeadZone(x, y, deadZone());
    el("bd-pad-g" + n).textContent = seen[0] === 0 && seen[1] === 0 ? "no movement" : "x " + seen[0].toFixed(2) + ", y " + seen[1].toFixed(2);
    drawStick(n, x, y);
    var k = svg.querySelector('[data-k="' + n + '"]');
    if (k && standard) {
      var cx = n === 0 ? 128 : 262, cy = n === 0 ? 104 : 160;
      k.setAttribute("cx", (cx + Math.max(-1, Math.min(1, x)) * 13).toFixed(1));
      k.setAttribute("cy", (cy + Math.max(-1, Math.min(1, y)) * 13).toFixed(1));
    }
  }

  function update(gp) {
    var pressedNow = [];
    gp.buttons.forEach(function (b, i) {
      var v = typeof b === "object" ? b.value : b, p = typeof b === "object" ? b.pressed : b > 0.5;
      if (p) { pressedNow.push(i); }
      var it = raw.querySelector('[data-rb="' + i + '"]');
      if (it) {
        it.classList.toggle("on", p);
        it.querySelector("em").textContent = v.toFixed(2);
        it.querySelector("i").style.width = Math.round(v * 100) + "%";
      }
      if (standard) {
        var s = svg.querySelector('[data-b="' + i + '"]');
        if (s) { s.classList.toggle("on", p); }
        var bar = svg.querySelector('[data-bar="' + i + '"]');
        if (bar) { bar.setAttribute("width", (62 * Math.max(0, Math.min(1, v))).toFixed(1)); }
      }
    });
    pressedNow.forEach(function (i) {
      if (lastPressed.indexOf(i) === -1) {
        el("bd-pad-last").textContent = "Last pressed: " + buttonName(brand, i, standard) + " (button " + i + ")";
      }
    });
    lastPressed = pressedNow;
    var k0 = svg.querySelector('[data-k="0"]'), k1 = svg.querySelector('[data-k="1"]');
    if (k0) { k0.classList.toggle("on", pressedNow.indexOf(10) !== -1); }
    if (k1) { k1.classList.toggle("on", pressedNow.indexOf(11) !== -1); }
    gp.axes.forEach(function (a, i) {
      var it = raw.querySelector('[data-ra="' + i + '"]');
      if (!it) { return; }
      it.querySelector("em").textContent = (a >= 0 ? "+" : "") + a.toFixed(3);
      var w = Math.min(50, Math.abs(a) * 50), bar = it.querySelector("i");
      bar.style.width = w + "%";
      bar.style.left = (a < 0 ? 50 - w : 50) + "%";
      it.classList.toggle("on", Math.abs(a) > 0.5);
    });
    showStick(0, gp);
    showStick(1, gp);
    var axes = stickAxes();
    if (drift) {
      [0, 1].forEach(function (n) {
        if (gp.axes.length > axes[n][1]) { drift.samples[n].push([gp.axes[axes[n][0]], gp.axes[axes[n][1]]]); }
      });
    }
    if (reach) {
      [0, 1].forEach(function (n) {
        if (gp.axes.length > axes[n][1]) { reach[n].push([gp.axes[axes[n][0]], gp.axes[axes[n][1]]]); }
      });
    }
  }

  function loop() {
    var list = pads();
    if (!list.length) {
      current = null; shownKey = "";
      wait.hidden = false; main.hidden = true;
      if (drift) { finishDrift(true); }
      if (reach) { stopReach(true); }
      window.setTimeout(function () { window.requestAnimationFrame(loop); }, 250);
      return;
    }
    var gp = null;
    list.forEach(function (p) { if (p.index === current) { gp = p; } });
    if (!gp) { gp = list[0]; current = gp.index; }
    wait.hidden = true; main.hidden = false;
    drawTabs(list);
    var key = gp.index + "|" + gp.id + "|" + gp.mapping + "|" + gp.buttons.length + "|" + gp.axes.length;
    if (key !== shownKey) { shownKey = key; setup(gp); }
    update(gp);
    window.requestAnimationFrame(loop);
  }

  /* Drift check: 3 seconds of samples with the sticks untouched. */
  function finishDrift(cancelled) {
    var d = drift;
    drift = null;
    el("bd-pad-dgo").disabled = false;
    el("bd-pad-dmsg").textContent = "Let go of both sticks first. The check takes 3 seconds.";
    if (cancelled || !d) { el("bd-pad-dres").innerHTML = ""; return; }
    var dz = deadZone(), out = "", names = standard ? ["Left stick", "Right stick"] : ["Axes 0 and 1", "Axes 2 and 3"];
    [0, 1].forEach(function (n) {
      if (!d.samples[n].length) { return; }
      var st = driftStats(d.samples[n]), v = driftVerdict(st, dz);
      var where = "At rest it sits " + pct(st.offset) + " from the centre (x " + (st.mx >= 0 ? "+" : "") + st.mx.toFixed(3) + ", y " + (st.my >= 0 ? "+" : "") + st.my.toFixed(3) + ") and went up to " + pct(st.maxR) + " during the check.";
      var cls = "ok", head = "", tail = "";
      if (v === "moved") { cls = "warn"; head = "Moved a long way during the check"; tail = "The reading went past half of the stick's full movement. Usually that means something touched it: put the controller on a table and check again. If it keeps happening, the stick may be badly faulty."; where = ""; }
      else if (v === "ok") { head = "No drift"; tail = "It stays well inside a " + pct(dz) + " dead zone, so a game with that dead zone will not see it move."; }
      else if (v === "borderline") { cls = "warn"; head = "Close to drifting"; tail = "It stays inside a " + pct(dz) + " dead zone, but went more than halfway to its edge. A game with a smaller dead zone may see slow movement."; }
      else { cls = "bad"; head = "Drift"; tail = "It goes outside a " + pct(dz) + " dead zone without being touched, so a game with that dead zone may see movement."; }
      if (v !== "moved" && st.spread > 0.03) { tail += " The reading also wanders by up to " + pct(st.spread) + " on its own, which can be a sign of a worn or dirty stick."; }
      out += '<div class="bd-pad-v ' + cls + '"><strong>' + names[n] + ": " + head + "</strong>" + (where ? where + " " : "") + tail + "</div>";
    });
    el("bd-pad-dres").innerHTML = out || '<div class="bd-pad-v">This controller has no analogue sticks to check.</div>';
  }
  el("bd-pad-dgo").addEventListener("click", function () {
    if (drift || current === null) { return; }
    drift = { samples: [[], []] };
    el("bd-pad-dgo").disabled = true;
    el("bd-pad-dres").innerHTML = "";
    var left = 3;
    el("bd-pad-dmsg").textContent = "Hands off the sticks. Measuring: " + left;
    var tick = window.setInterval(function () {
      left--;
      if (!drift) { window.clearInterval(tick); return; }
      if (left <= 0) { window.clearInterval(tick); finishDrift(false); return; }
      el("bd-pad-dmsg").textContent = "Hands off the sticks. Measuring: " + left;
    }, 1000);
  });
  el("bd-pad-dz").addEventListener("input", function () { el("bd-pad-dzv").textContent = el("bd-pad-dz").value + "%"; });

  /* Reach check: the edge of each stick's travel. */
  function stopReach(cancelled) {
    var pts = reach;
    reach = null;
    el("bd-pad-rgo").textContent = "Start";
    el("bd-pad-rmsg").textContent = "";
    if (cancelled || !pts) { return; }
    var out = "", names = standard ? ["Left stick", "Right stick"] : ["Axes 0 and 1", "Axes 2 and 3"];
    [0, 1].forEach(function (n) {
      if (!pts[n].length) { return; }
      var rs = reachStats(pts[n]), v = reachVerdict(rs);
      reachDone[n] = rs.covered ? rs : null;
      if (v === "incomplete") {
        out += '<div class="bd-pad-v warn"><strong>' + names[n] + ": not enough of the edge</strong>Roll the stick all the way round its edge, slowly, and try again.</div>";
      } else if (v === "short") {
        out += '<div class="bd-pad-v bad"><strong>' + names[n] + ": does not reach the edge " + dirName(rs.minAngle) + "</strong>Pushed fully " + dirName(rs.minAngle) + ", it only reaches " + pct(rs.min) + ", so a game may never get full speed in that direction. The red line above shows the shape it reached.</div>";
      } else {
        out += '<div class="bd-pad-v ok"><strong>' + names[n] + ": reaches the edge all round</strong>Its weakest direction is " + dirName(rs.minAngle) + " at " + pct(rs.min) + ". Some controllers go past 100% at the diagonals, which is normal.</div>";
      }
    });
    el("bd-pad-rres").innerHTML = out || '<div class="bd-pad-v">This controller has no analogue sticks to check.</div>';
  }
  el("bd-pad-rgo").addEventListener("click", function () {
    if (current === null) { return; }
    if (reach) { stopReach(false); return; }
    reach = [[], []];
    reachDone = [null, null];
    el("bd-pad-rres").innerHTML = "";
    el("bd-pad-rgo").textContent = "Finish";
    el("bd-pad-rmsg").textContent = "Roll each stick slowly round its edge, then press Finish.";
  });

  /* Vibration */
  el("bd-pad-vrow").addEventListener("click", function (e) {
    var b = e.target.closest("button[data-m]");
    if (!b || current === null) { return; }
    var gp = null;
    pads().forEach(function (p) { if (p.index === current) { gp = p; } });
    var m = b.getAttribute("data-m"), type = "dual-rumble", params;
    if (m === "triggers") {
      // The small motors in the triggers of Xbox One and Series controllers.
      type = "trigger-rumble";
      params = { startDelay: 0, duration: 1000, strongMagnitude: 0, weakMagnitude: 0, leftTrigger: 1, rightTrigger: 1 };
    } else {
      params = { startDelay: 0, duration: 1000, weakMagnitude: m === "strong" ? 0 : 1, strongMagnitude: m === "weak" ? 0 : 1 };
    }
    if (!gp || !canEffect(gp, type)) { return; }
    var say = function (t) { el("bd-pad-vmsg").textContent = t; };
    say("Vibrating for one second.");
    gp.vibrationActuator.playEffect(type, params).then(function (result) {
      if (result === "preempted") { say("That vibration was stopped by another one. Press a button again."); return; }
      say("Your browser says it sent the vibration. If you felt nothing, either the motor is not working or this browser cannot drive it over this connection. To tell which, try a USB cable or another browser (on a Mac, try Safari), or a game you know vibrates.");
    }, function (err) {
      say("Your browser refused the vibration request" + (err && err.name ? " (" + err.name + ")" : "") + ". Try a USB cable, or another browser.");
    });
  });

  window.addEventListener("gamepadconnected", function () { shownKey = ""; });
  window.addEventListener("gamepaddisconnected", function () { shownKey = ""; });
  window.requestAnimationFrame(loop);
})();
</script>
</div>

Get new Braindumps guides by email

One email a week with new guides, the best free 3D prints, LEGO news and tools. Unsubscribe at any time.

No spam. Read our privacy policy.