mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 14:50:00 -07:00
Smooth proximity radar positions with EMA and CSS transitions
The remaining jitter after the in-place DOM rewrite was caused by RSSI fluctuations propagating directly into dot positions on every 200ms update cycle. Two fixes: 1. Client-side EMA (alpha=0.25) on x/y coordinates per device. Each render blends 25% toward the new raw position and retains 75% of the smoothed position, filtering high-frequency RSSI noise without hiding genuine distance changes. positionCache is keyed by device_key and cleared on device removal or radar reset. 2. CSS transition (transform 0.6s ease-out) on each wrapper element. Switching from SVG transform attribute to style.transform enables native CSS transitions, so any remaining position change (e.g. a band crossing) animates smoothly rather than snapping. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,10 +25,14 @@ const ProximityRadar = (function() {
|
||||
newDeviceThreshold: 30, // seconds
|
||||
};
|
||||
|
||||
// Configuration
|
||||
const POSITION_EMA_ALPHA = 0.25; // lower = more smoothing (0.25 → ~4 updates to reach 68% of a step)
|
||||
|
||||
// State
|
||||
let container = null;
|
||||
let svg = null;
|
||||
let devices = new Map();
|
||||
let positionCache = new Map(); // device_key → { x, y } smoothed position
|
||||
let isPaused = false;
|
||||
let activeFilter = null;
|
||||
let onDeviceClick = null;
|
||||
@@ -213,13 +217,24 @@ const ProximityRadar = (function() {
|
||||
|
||||
// Remove elements for devices no longer in the visible set
|
||||
devicesGroup.querySelectorAll('.radar-device-wrapper').forEach(el => {
|
||||
if (!visibleKeys.has(el.getAttribute('data-device-key'))) {
|
||||
const k = el.getAttribute('data-device-key');
|
||||
if (!visibleKeys.has(k)) {
|
||||
positionCache.delete(k);
|
||||
el.remove();
|
||||
}
|
||||
});
|
||||
|
||||
visibleDevices.forEach(device => {
|
||||
const { x, y } = calculateDevicePosition(device, center, maxRadius);
|
||||
// Raw target position from distance/band
|
||||
const { x: rawX, y: rawY } = calculateDevicePosition(device, center, maxRadius);
|
||||
|
||||
// EMA smoothing: blend towards the new position rather than snapping,
|
||||
// so RSSI noise doesn't translate 1:1 into visible movement.
|
||||
const cached = positionCache.get(device.device_key);
|
||||
const x = cached ? cached.x * (1 - POSITION_EMA_ALPHA) + rawX * POSITION_EMA_ALPHA : rawX;
|
||||
const y = cached ? cached.y * (1 - POSITION_EMA_ALPHA) + rawY * POSITION_EMA_ALPHA : rawY;
|
||||
positionCache.set(device.device_key, { x, y });
|
||||
|
||||
const confidence = device.distance_confidence || 0.5;
|
||||
const dotSize = CONFIG.dotMinSize + (CONFIG.dotMaxSize - CONFIG.dotMinSize) * confidence;
|
||||
const color = getBandColor(device.proximity_band);
|
||||
@@ -234,7 +249,7 @@ const ProximityRadar = (function() {
|
||||
|
||||
if (existing) {
|
||||
// ── In-place update: mutate attributes, never recreate ──
|
||||
existing.setAttribute('transform', `translate(${x}, ${y})`);
|
||||
existing.style.transform = `translate(${x}px, ${y}px)`;
|
||||
|
||||
const innerG = existing.querySelector('.radar-device');
|
||||
if (innerG) {
|
||||
@@ -291,7 +306,8 @@ const ProximityRadar = (function() {
|
||||
const wrapperG = document.createElementNS(ns, 'g');
|
||||
wrapperG.classList.add('radar-device-wrapper');
|
||||
wrapperG.setAttribute('data-device-key', key);
|
||||
wrapperG.setAttribute('transform', `translate(${x}, ${y})`);
|
||||
wrapperG.style.transform = `translate(${x}px, ${y}px)`;
|
||||
wrapperG.style.transition = 'transform 0.6s ease-out';
|
||||
|
||||
const innerG = document.createElementNS(ns, 'g');
|
||||
innerG.classList.add('radar-device');
|
||||
@@ -446,6 +462,7 @@ const ProximityRadar = (function() {
|
||||
*/
|
||||
function clear() {
|
||||
devices.clear();
|
||||
positionCache.clear();
|
||||
selectedDeviceKey = null;
|
||||
renderDevices();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user