White landing ring: closed-form prediction kills the jitter

predictLanding stepped the trajectory in 10ms chunks and returned the first
point past ground level — an overshoot of up to ~25cm that varied per frame,
which is what actually jittered (the earlier pixel-snap only addressed
sub-pixel shimmer). The landing point of a parabola has an exact solution;
computing it directly makes the prediction identical every frame.
Measured: max frame-to-frame movement 5.5mm, was ~25cm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-07-11 15:25:43 +02:00
1 parent 0bf6937121
commit f2bf21f252
1 file changed
+7 -9
+7 -9
View File
@@ -579,14 +579,12 @@ function update(dt) {
if (phase === 'over') { overT -= dt; if (overT <= 0) setupPoint(); } if (phase === 'over') { overT -= dt; if (overT <= 0) setupPoint(); }
} }
function predictLanding() { // first-bounce point of the ball's current trajectory function predictLanding() { // first-bounce point, closed form — exact, so the ring never jitters
let { x, y, z, vx, vy, vz } = ball; const g = ball.g || TUNE.gravity;
const g = ball.g || TUNE.gravity, dt = 0.01; const disc = ball.vy * ball.vy + 2 * g * ball.y;
for (let i = 0; i < 300; i++) { if (disc < 0) return null;
vy -= g * dt; x += vx * dt; y += vy * dt; z += vz * dt; const t = (ball.vy + Math.sqrt(disc)) / g; // time until y = 0
if (y <= 0) return { x, z }; return { x: ball.x + ball.vx * t, z: ball.z + ball.vz * t };
}
return null;
} }
// ---------------------------------------------------------------- render ---- // ---------------------------------------------------------------- render ----
@@ -739,7 +737,7 @@ function render() {
// server idly bouncing the ball while waiting // server idly bouncing the ball while waiting
if (phase === 'pretoss') { if (phase === 'pretoss') {
const sv = score.server === 'P' ? P : O; const sv = score.server === 'P' ? P : O;
const bx = sv.x - 0.5 * (sv.side === 'P' ? 1 : -1), by = Math.abs(Math.sin(idleT * 3.6)) * 0.7; // floor to the reaching hand const bx = sv.x - 0.5 * (sv.side === 'P' ? 1 : -1), by = Math.abs(Math.sin(idleT * 3.6)) * 0.55; // floor to the reaching hand
const sh = proj(bx, 0, sv.z); const sh = proj(bx, 0, sv.z);
ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.beginPath(); ctx.ellipse(sh.x, sh.y, Math.max(1.5, sh.s * 0.08), Math.max(1, sh.s * 0.035), 0, 0, 7); ctx.fill(); ctx.beginPath(); ctx.ellipse(sh.x, sh.y, Math.max(1.5, sh.s * 0.08), Math.max(1, sh.s * 0.035), 0, 0, 7); ctx.fill();