Diagnosed ghost rectangle not tracking position in space: two root
causes — motionHandler never called
feedGhostGyro (so yaw integral was always 0), and
window.__GIT_COMMIT__ bypassed Vite's define substitution
(badge showed "v dev")
Fixed both: routed gyro calls from devicemotion into
feedGhostGyro, changed window.__GIT_COMMIT__ →
bare __GIT_COMMIT__
Added vertical pitch compensation: imported
computeShiftPy, wired pitch integral into
latestShiftPy, applied top as well as
left in the RAF loop — rectangle now tracks both yaw
(horizontal) and pitch (vertical)
Fixed frames[] always empty: SensorFrame
push moved into motionHandler so it fires without Web
Gyroscope API
Added betaDeg tracking via
deviceorientation to gate accelerometer integration
near-horizontal
Recorded debug JSON confirmed pitchShiftPx = 0 on all
frames before fix; confirmed shiftPx working
ro5u review identified 3 bugs: (1) double gyro integration when
deps.gyro present — both motionHandler +
onGyroReading call feedGhostGyro with no
mutual exclusion guard, (2) double SensorFrame push on same
paths, (3) minor: two nowFn() calls in one handler
tick
Next: apply the ro5u fixes — add
!this.gyro guard to motionHandler's gyro
integration and frame push paths; deduplicate timestamp read
Session 20:33 (miblioteca)
Diagnosed ghost square drifting upward: ay bias
(~−0.035 m/s²) double-integrates over 4–6s → false 50cm downward
displacement → ghost shifts up 300+px. Firefox Android returns
null for beta, defaulting to 90 → tilt guard
never fires
Fix A (null-beta guard): getBeta() returning null now
skips feedGhostAccel and zeroes velocity;
getBeta type changed to
() => number | null
Fix C (dead-zone): skip integration + zero velocity when both
|ax|<0.05 AND |ay|<0.05; always update
lastAccelT for accurate dt
Second calibration (19:11 landscape vs 19:13 portrait): landscape
stable, portrait still drifting → H2 confirmed — no axis transform for
accel (code always uses motion.y as screen-Y, but in
landscape device-Y maps to screen-X)
Orientation remap: in landscape,
ax = motion.y, ay = -motion.x before feeding to
feedGhostAccel; mirrors the gyro path's
scanAxis pattern
Pipeline observability refactor: feedGhostAccel now
returns
AccelFeedResult { state, gate: 'pass'|'dead-zone'|'tilt' }
instead of bare state; SensorFrame gains
betaDeg, gate, velX,
velY, dx_cm, dy_cm;
CalibrationExport gains orientation;
GhostCalibrationPage injects
getOrientation
Third calibration (22:00) showed residual portrait drift → H3
confirmed (dead zone too permissive: ~40% of ay≈−0.039
samples escape |ay|<0.05); discussed open-loop vs PID:
current pipeline is pure double-integration with no feedback
ZUPT refactor: replaced AND dead-zone with L2-norm threshold (0.10
m/s²) + exponential decay (τ=200ms); renamed gate
'dead-zone' → 'zupt'; hoisted dt
computation before all gate checks
Three tickets created (bdx, dk8, oxy), issue-review run, folded into
one implementation and closed; 630/630 tests pass, 2 commits pushed
Next: collect new calibration data with pipeline
logging to verify ZUPT eliminates portrait drift
Tasks created
Decisions
computeShiftPy uses viewport height (vh) as the
focal-length proxy for pitch, matching how computeShiftPx
uses vw for yaw
Default workingDistanceCm = 60 injected as dep,
overridable via add-distance-config spec later