Calibration vs. Confidence
About This Sketch
Eighteen particles — nine overconfident, nine calibrated — each chasing a fixed target. On the left, high ambition and internal noise create perpetual oscillation: always moving, never arriving, color warming with each overshoot. On the right, steady convergence: each particle finds its mark and rests there, pulsing softly.
Algorithm
Two panels compare calibration dynamics. Left: overconfident particles use a high-gain spring force plus sinusoidal noise injection, producing perpetual oscillation — they approach targets but never settle, and their color shifts warmer as distance from target grows, encoding error in real time. Right: calibrated particles use a gentle damped spring that converges precisely to targets; once settled, each particle pulses softly with a glowing ring, indicating stable accuracy.
Pseudocode
SETUP:
Place 9 particles in 3x3 grid on each panel half
Each particle starts at target position + random offset
Assign random phase for desynchronized motion
DRAW:
Left panel (overconfident):
For each particle:
Compute displacement toward target
Apply high-gain spring + phase-shifted sinusoidal noise
Update position with low damping coefficient
Constrain to left panel bounds
Map distance from target to color (warm = far, cool = near)
Draw target ring and colored particle
Right panel (calibrated):
For each particle:
Compute displacement toward target
If not settled: apply low-gain spring with high damping
If settled: pulse gently at target with sub-pixel offset
Draw target ring (glowing when settled) and steady particle
Source Code
let sketch = function(p) {
const W = 400, H = 300, MID = W / 2;
let lp = [], rp = [];
p.setup = function() {
p.createCanvas(W, H);
p.colorMode(p.RGB);
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 3; col++) {
let ltx = 30 + col * 55;
let lty = 55 + row * 80;
lp.push({
x: ltx + p.random(-40, 40),
y: lty + p.random(-28, 28),
tx: ltx, ty: lty,
vx: p.random(-2, 2),
vy: p.random(-2, 2),
ph: p.random(p.TWO_PI)
});
let rtx = 230 + col * 55;
let rty = 55 + row * 80;
rp.push({
x: rtx + p.random(-22, 22),
y: rty + p.random(-22, 22),
tx: rtx, ty: rty,
vx: 0, vy: 0,
ph: p.random(p.TWO_PI),
settled: false
});
}
}
};
p.draw = function() {
const c = getThemeColors();
p.background(...c.bg);
let t = p.frameCount * 0.022;
p.stroke(...c.accent3, 45);
p.strokeWeight(0.5);
p.line(MID, 16, MID, H - 8);
p.noStroke();
p.fill(...c.accent3);
p.textAlign(p.CENTER);
p.textSize(9);
p.text("overconfident", MID / 2, 13);
p.text("calibrated", MID + (W - MID) / 2, 13);
for (let pt of lp) {
let dx = pt.tx - pt.x;
let dy = pt.ty - pt.y;
let noise = 1.5 + p.sin(t * 2.3 + pt.ph) * 1.2;
pt.vx = pt.vx * 0.77 + dx * 0.12 + p.cos(t * 1.9 + pt.ph) * noise;
pt.vy = pt.vy * 0.77 + dy * 0.12 + p.sin(t * 2.1 + pt.ph * 0.9) * noise;
pt.x = p.constrain(pt.x + pt.vx, 6, MID - 6);
pt.y = p.constrain(pt.y + pt.vy, 20, H - 6);
let dist = p.dist(pt.x, pt.y, pt.tx, pt.ty);
let err = p.constrain(dist / 36, 0, 1);
p.noFill();
p.stroke(...c.accent3, 80);
p.strokeWeight(1);
p.circle(pt.tx, pt.ty, 14);
p.noStroke();
p.fill(
p.lerp(c.accent1[0], c.accent2[0], err),
p.lerp(c.accent1[1], c.accent2[1], err),
p.lerp(c.accent1[2], c.accent2[2], err),
210
);
p.circle(pt.x, pt.y, 6);
}
for (let pt of rp) {
let dx = pt.tx - pt.x;
let dy = pt.ty - pt.y;
let dist = p.dist(pt.x, pt.y, pt.tx, pt.ty);
if (dist > 1.8) {
pt.vx = pt.vx * 0.89 + dx * 0.036;
pt.vy = pt.vy * 0.89 + dy * 0.036;
pt.x += pt.vx;
pt.y += pt.vy;
} else {
pt.settled = true;
pt.x = pt.tx + p.sin(t * 1.3 + pt.ph) * 0.7;
pt.y = pt.ty + p.cos(t * 1.1 + pt.ph) * 0.7;
}
p.noFill();
if (pt.settled) {
let alpha = 50 + p.sin(t * 1.3 + pt.ph) * 22;
p.stroke(...c.accent1, alpha);
p.strokeWeight(1.5);
p.circle(pt.tx, pt.ty, 20);
} else {
p.stroke(...c.accent3, 80);
p.strokeWeight(1);
p.circle(pt.tx, pt.ty, 14);
}
p.noStroke();
p.fill(...c.accent1, 210);
p.circle(pt.x, pt.y, 6);
}
};
};