<div class="controls">
<label>Particles: <span id="nLabel">300</span>
<input type="range" id="nSlider" min="50" max="800" value="300" step="50">
</label>
<label>Noise: <span id="noiseLabel">30</span>
<input type="range" id="noiseSlider" min="5" max="80" value="30" step="5">
</label>
<div class="btns">
<button id="pauseBtn" type="button">Pause</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="canvas" width="480" height="300"></canvas>
<div id="info" class="info">Runningโฆ watch the cloud converge on the blue dot.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; align-items: center; margin-bottom: .5rem; font-size: .85rem; }
label { display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 90px; accent-color: #e07b39; }
.btns { display: flex; gap: .5rem; margin-left: auto; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #dde3e8; border-radius: 8px; max-width: 100%; }
.info { font-size: .82rem; color: #555; margin-top: .4rem; min-height: 1.3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Particle Filter demo โ tracks a random-walking target with noisy range observations.
// Algorithm: predict โ weight โ resample (SIR / Bootstrap filter).
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// โโ Parameters (read from sliders) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let N = 300; // number of particles
let SENSOR_NOISE = 30; // std-dev of range sensor (pixels)
const PROCESS_NOISE = 4; // std-dev of random-walk step (pixels)
const TARGET_SPEED = 2; // pixels per frame for target movement
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let target = { x: W/2, y: H/2, vx: TARGET_SPEED, vy: TARGET_SPEED };
let particles = []; // [{x, y, w}]
let paused = false;
let animId = null;
let frame = 0;
// โโ Gaussian random (Box-Muller) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function randn() {
let u, v;
do { u = Math.random(); } while (u === 0);
do { v = Math.random(); } while (v === 0);
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// โโ Initialise particles uniformly across canvas โโโโโโโโโโโโโโโโโโโโโโโโ
function initParticles() {
particles = [];
for (let i = 0; i < N; i++) {
particles.push({ x: Math.random() * W, y: Math.random() * H, w: 1 / N });
}
}
// โโ Move target (bounce off walls) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function stepTarget() {
// Small random turn
target.vx += randn() * 0.5;
target.vy += randn() * 0.5;
// Clamp speed
const spd = Math.hypot(target.vx, target.vy);
if (spd > TARGET_SPEED * 1.5) { target.vx *= TARGET_SPEED / spd; target.vy *= TARGET_SPEED / spd; }
target.x += target.vx;
target.y += target.vy;
// Bounce
if (target.x < 8) { target.x = 8; target.vx *= -1; }
if (target.x > W-8){ target.x = W-8; target.vx *= -1; }
if (target.y < 8) { target.y = 8; target.vy *= -1; }
if (target.y > H-8){ target.y = H-8; target.vy *= -1; }
}
// โโ Sensor: noisy range from a fixed corner beacon โโโโโโโโโโโโโโโโโโโโโโ
const BEACON = { x: 0, y: 0 }; // top-left corner
function trueRange() {
return Math.hypot(target.x - BEACON.x, target.y - BEACON.y);
}
function noisyObs() {
return trueRange() + randn() * SENSOR_NOISE;
}
function likelihood(px, py, obs) {
const d = Math.hypot(px - BEACON.x, py - BEACON.y);
const diff = d - obs;
return Math.exp(-0.5 * diff * diff / (SENSOR_NOISE * SENSOR_NOISE));
}
// โโ Predict: jitter each particle by process noise โโโโโโโโโโโโโโโโโโโโโโ
function predict() {
for (const p of particles) {
p.x += randn() * PROCESS_NOISE;
p.y += randn() * PROCESS_NOISE;
p.x = Math.max(0, Math.min(W, p.x));
p.y = Math.max(0, Math.min(H, p.y));
}
}
// โโ Weight: Gaussian likelihood of the observation โโโโโโโโโโโโโโโโโโโโโโ
function weight(obs) {
let total = 0;
for (const p of particles) {
p.w = likelihood(p.x, p.y, obs);
total += p.w;
}
if (total === 0) { for (const p of particles) p.w = 1 / N; return; }
for (const p of particles) p.w /= total;
}
// โโ Resample: systematic resampling โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function resample() {
const newP = [];
const step = 1 / N;
let r = Math.random() * step;
let cumW = particles[0].w;
let j = 0;
for (let i = 0; i < N; i++) {
while (r > cumW && j < N - 1) { j++; cumW += particles[j].w; }
newP.push({ x: particles[j].x, y: particles[j].y, w: 1 / N });
r += step;
}
particles = newP;
}
// โโ Estimate: weighted mean position โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function estimate() {
let ex = 0, ey = 0;
for (const p of particles) { ex += p.x * p.w; ey += p.y * p.w; }
return { x: ex * N, y: ey * N }; // weights already normalised to 1/N after resample
}
// โโ Draw โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw(obs) {
ctx.clearRect(0, 0, W, H);
// Background grid
ctx.strokeStyle = '#f0f3f6';
ctx.lineWidth = 1;
for (let x = 0; x <= W; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
for (let y = 0; y <= H; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }
// Sensor range ring (noisy observation)
ctx.strokeStyle = 'rgba(100,120,180,0.35)';
ctx.setLineDash([5, 5]);
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(BEACON.x, BEACON.y, obs, 0, 2 * Math.PI);
ctx.stroke();
ctx.setLineDash([]);
// Beacon
ctx.fillStyle = '#8899bb';
ctx.beginPath();
ctx.arc(BEACON.x, BEACON.y, 5, 0, 2 * Math.PI);
ctx.fill();
// Particles
for (const p of particles) {
ctx.globalAlpha = 0.55;
ctx.fillStyle = '#e07b39';
ctx.beginPath();
ctx.arc(p.x, p.y, 2.5, 0, 2 * Math.PI);
ctx.fill();
}
ctx.globalAlpha = 1;
// Estimated position (weighted mean)
const est = estimate();
ctx.strokeStyle = '#e07b39';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(est.x, est.y, 8, 0, 2 * Math.PI);
ctx.stroke();
// True target
ctx.fillStyle = '#1d6fb8';
ctx.beginPath();
ctx.arc(target.x, target.y, 7, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(target.x, target.y, 3, 0, 2 * Math.PI);
ctx.fill();
// Legend
ctx.font = '11px system-ui';
ctx.fillStyle = '#1d6fb8';
ctx.fillText('โ Target', W - 90, 16);
ctx.fillStyle = '#e07b39';
ctx.fillText('โ Particles / estimate', W - 150, 30);
ctx.fillStyle = '#8899bb';
ctx.fillText('โ Sensor beacon', W - 150, 44);
// Error
const err = Math.hypot(est.x - target.x, est.y - target.y).toFixed(1);
document.getElementById('info').textContent =
fmt('Frame %frame% | Sensor noise ฯ=%SENSOR_NOISE%px | Estimate error: %err%px | %N% particles', { frame: frame, SENSOR_NOISE: SENSOR_NOISE, err: err, N: N });
}
// โโ Main loop โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function loop() {
if (!paused) {
stepTarget();
const obs = noisyObs();
predict();
weight(obs);
resample();
frame++;
draw(obs);
}
animId = requestAnimationFrame(loop);
}
function reset() {
frame = 0;
target = { x: W/2, y: H/2, vx: TARGET_SPEED, vy: 0.5 };
N = parseInt(document.getElementById('nSlider').value);
SENSOR_NOISE = parseInt(document.getElementById('noiseSlider').value);
initParticles();
}
// โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.getElementById('pauseBtn').addEventListener('click', function() {
paused = !paused;
this.textContent = paused ? 'Resume' : 'Pause';
});
document.getElementById('resetBtn').addEventListener('click', function() {
reset();
if (paused) { paused = false; document.getElementById('pauseBtn').textContent = 'Pause'; }
});
document.getElementById('nSlider').addEventListener('input', function() {
N = parseInt(this.value);
document.getElementById('nLabel').textContent = N;
initParticles();
});
document.getElementById('noiseSlider').addEventListener('input', function() {
SENSOR_NOISE = parseInt(this.value);
document.getElementById('noiseLabel').textContent = SENSOR_NOISE;
});
// โโ Start โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
reset();
loop();
Comments
Loading comments...