fix(humanize): preserve gaussian magnitude in jitter_point; unify swipe return shape

This commit is contained in:
2026-07-15 19:58:02 +08:00
parent 5a93651db7
commit 9da73cc6e3
3 changed files with 42 additions and 10 deletions
+18 -10
View File
@@ -90,23 +90,31 @@ def set_rng(rng: random.Random | None) -> None:
def jitter_point(
x: float, y: float, *, radius: float, rng: random.Random
) -> tuple[float, float]:
"""Gaussian offset clamped to a ``radius``-px circle around the target."""
"""Polar-Gaussian offset around the target.
Magnitude is drawn from a folded Gaussian with sigma=radius/2, then
capped at ``radius``. The angle is uniform on [0, 2*pi). This concentrates
jittered points near the target rather than uniformly on the circle edge,
matching how humans tap close to (but not exactly on) a button center.
"""
r = abs(rng.gauss(0.0, radius / 2.0))
r = min(r, radius)
angle = rng.uniform(0, 2 * math.pi)
dx = r * math.cos(angle)
dy = r * math.sin(angle)
# Reproject to exact radius to absorb FP drift in cos/sin
d = math.sqrt(dx * dx + dy * dy)
if d > 0:
dx = dx / d * radius
dy = dy / d * radius
# Guard against cos/sin FP drift pushing distance slightly past radius.
fd = math.hypot(dx, dy)
if fd > radius:
scale = radius / fd
dx *= scale
dy *= scale
# Final clamp: hypot of the returned offset may overshoot by ulps; tighten
# to ``radius - 1e-10`` to keep ``<= radius`` after subtraction/hypot.
fx = x + dx
fy = y + dy
fd = math.hypot(fx - x, fy - y)
if fd > radius:
# Scale to a slightly tighter radius to absorb FP rounding in addition
scale = (radius - 1e-10) / fd
fd_final = math.hypot(fx - x, fy - y)
if fd_final > radius:
scale = (radius - 1e-10) / fd_final
fx = x + dx * scale
fy = y + dy * scale
return (fx, fy)