Your bat never leaves one flat, forward-leaning plane, and its face is fixed — you cannot open it, close it, or angle it. Everything you do is the velocity the bat happens to have at the instant of contact.
Move the cursor over the table and the bat goes there. On a phone, drag. How fast you move IS how hard you brush — flick upward through the ball and it comes off with topspin, which is the only thing that will bring a hard shot down in time.
It runs at half speed. Real table tennis is not a game you can read at full rate through a mouse, and the speed control under the table goes back to 1× when you want it.
On a phone the bat sits above your thumb so you can see what you are hitting — the bat lift slider sets how far.
Still four keys if you want them: Q up the plane, W down it, O left, P right.
A ball does not have a lift coefficient. It has a boundary layer,
and when the surface is turning, that layer is dragged further round one side
than the other, so the two separation points sit at different angles and the
wake leaves at a slant. The reaction to that slanted wake is the Magnus
force. solver/ in this directory is a D2Q9 lattice Boltzmann
solver in Rust, compiled to a 34 kB WebAssembly module, that solves that
flow past a rotating cylinder and measures the momentum the fluid hands over.
Nothing in it knows the word Magnus.
The panel over the table is that solver running live at whatever spin ratio the ball currently has — a smaller, cheaper grid than the one that produced the numbers, so it is an illustration rather than the measurement. The coefficients the flight actually uses come from a long converged sweep, and what the sweep is and is not honest about is in README.md.
Brush gently and the ball goes into the net; brush hard and it sails long — except that brushing hard is also what puts topspin on it, and topspin is what pulls it back down. The window between those is the game, and it exists because of the aerodynamics rather than in spite of them. With the solved lift that window is 3.25 m/s wide; with the lift switched off and everything else identical it is 1.85. The same 9 m/s brush that lands 1.22 m past the net with the solved lift is 67 cm beyond the end line without it.
The plane leans forward 30° and the face is closed 12°, and both of those are load-bearing. The first version had a vertical plane, which is the obvious reading of "the bat is stuck in a plane" — and the rally died in one exchange, because air drag takes about 40% of a table tennis ball's speed in a single crossing and a bat that cannot advance has nothing to put back.
The bat has two degrees of freedom and lives in a plane, which is exactly what a pointer is, so the pointer drives it: the bat sits where your cursor is — found by casting a ray through the cursor onto the stroke plane, so it really is under your finger rather than approximately under it — and the speed you move at is the speed it brushes. That speed is smoothed over 45 ms, because raw frame-to-frame pointer deltas are metres per second of noise, and clamped to the same top speed the keys can reach, so a flick cannot put spin on the ball that the contact model was never measured against.
It started as four keys, like everything else on this surface, and four keys turned out to be too much to ask: you have to arrive in the right PLACE at the right SPEED out of a single integrator, and the legal band of brush speeds is about 3 m/s wide out of 13. The keys still work. They are just no longer the way in.
On a phone the thing you are aiming is underneath the thing you are aiming with. A 40 mm bat vanishes completely under a thumb, and you end up playing by feel with your own hand as the occluder. So the bat can sit a fixed number of screen pixels above the pointer, and the slider sets how many, because what a thumb covers is not something this page can know.
It is a constant offset applied to the sample point, so it moves the bat and leaves the brush alone — a constant differentiates to zero, and the selftest asserts exactly that. What it does cost is travel. Measured on a 328 px canvas at phone width, the band you can actually point at runs from bat height 0.86 m down to −0.18, about 0.0045 m per pixel; 64 px of lift spends 0.29 m of the bottom of that, leaving 0.78 m of the 1.22 m reach. At half speed, where the same hand movement buys twice the brush, that is plenty. At 1× with a large lift it starts to bite, and the honest answer is to use less lift or less speed.
The setting is remembered in this browser.
The page runs at half speed by default. That is one clock running slower, not the ball being slowed while the bat is left alone: wall time is turned into game time once, at the top of the step function, and everything after that — the flight, the contact, the rival, every number measured on this page — is in game seconds. The selftest asserts it, and asserts it the strict way: the same amount of game time delivered over twice the wall clock produces a bit-identical game, not a nearly-identical one.
What it does change is your hand. The bat is pinned to the cursor, so the same movement covers the same distance in half the game time and the brush it produces doubles — a 0.60 m flick that makes 4.3 m/s at full speed makes 8.5 at half. That is the whole benefit: a gentler, more readable stroke reaches the same shot, and you get twice as long to decide on it.
Same four as /qwop/, /graze/, /qgol/, /griddle/, /armline/ and /mimic/. Rendering is three.js r169 (MIT), vendored — no CDN, nothing fetched at runtime but the solver.