The Epistemic Cowardice Trap
About This Sketch
Particles drift through the middle zone between two poles, driven by a Perlin noise flow field. Most orbit indefinitely without committing to either side. Occasionally one breaks free, settles near a pole, and pulses with light — representing the moment of commitment that makes feedback and learning possible. The crowded, drifting middle visualizes the epistemic cost of strategic non-commitment: motion without progress.
Algorithm
Particles drift through a central zone via a Perlin noise flow field, kept away from the
edges by soft boundary forces. Two glowing poles sit on the left and right edges of the
canvas. Each particle has a latent preference for one pole, but soft-wall forces and the
noise field keep it circling indefinitely in the middle zone.
With a small, age-dependent probability each frame, a particle "commits" — it places a
settled dot near its preferred pole, then resets as a new uncommitted particle. Settled
dots pulse with a slow sine wave and fade out over time, replaced by new ones. The
visual result: a crowded middle of orbiting particles contrasted with sparse, glowing
clusters near the poles.
Accompanies the post "The Epistemic Cowardice Trap," which argues that strategic
non-commitment is a stable social equilibrium — comfortable, never-wrong, and epistemically
useless. Commitment is required to generate feedback and improve.
Pseudocode
SETUP:
Initialize canvas (400x300)
Create 82 particles in the middle zone with random velocities and pole preferences
DRAW (every frame):
Get current theme colors
Fade background slightly (trail effect)
Draw glowing poles (left: accent2, right: accent1)
For each settled dot:
Age it; fade in, pulse, fade out; remove if too old
Draw glowing dot near its committed pole
For each orbiting particle:
Sample Perlin noise field for direction
Lerp velocity toward noise direction
Apply soft-wall pushback near the middle-zone edges
Constrain to canvas
With small probability (increases with age):
Place settled dot near preferred pole
Reset particle as new uncommitted particle
Continue
Draw particle with color blended between accent colors by lateral position
Draw caption label
Source Code
let sketch = function(p) {
// Two attractor poles on left and right — positions A and B
// Particles drift through the middle zone, pulled toward both poles simultaneously
// Most particles orbit perpetually without committing to either pole
// Occasionally one breaks free, settles near a pole, and glows
// Represents: perpetual uncertainty is a stable orbit; commitment enables feedback and growth
let particles = [];
let settled = [];
let poleAx = 65, poleAy = 150;
let poleBx = 335, poleBy = 150;
let time = 0;
function newParticle() {
return {
x: p.random(110, 290),
y: p.random(45, 255),
vx: p.random(-1.3, 1.3),
vy: p.random(-1.3, 1.3),
age: 0,
preferA: p.random() < 0.5
};
}
p.setup = function() {
p.createCanvas(400, 300);
p.colorMode(p.RGB);
for (let i = 0; i < 82; i++) particles.push(newParticle());
};
p.draw = function() {
const colors = getThemeColors();
p.noStroke();
p.fill(...colors.bg, 35);
p.rect(0, 0, 400, 300);
time += 0.011;
// Draw pole A (left)
for (let r = 42; r >= 8; r -= 8) {
p.fill(...colors.accent2, p.map(r, 8, 42, 78, 0));
p.noStroke();
p.circle(poleAx, poleAy, r * 2);
}
p.fill(...colors.accent2, 215);
p.noStroke();
p.circle(poleAx, poleAy, 13);
// Draw pole B (right)
for (let r = 42; r >= 8; r -= 8) {
p.fill(...colors.accent1, p.map(r, 8, 42, 78, 0));
p.noStroke();
p.circle(poleBx, poleBy, r * 2);
}
p.fill(...colors.accent1, 215);
p.noStroke();
p.circle(poleBx, poleBy, 13);
// Draw and age settled (committed) particles
settled = settled.filter(function(s) {
s.age++;
if (s.age > 420) return false;
var fadeIn = Math.min(s.age / 28, 1);
var fadeOut = s.age > 360 ? Math.max(0, (420 - s.age) / 60) : 1;
var pulse = 0.68 + 0.32 * Math.sin(time * 2.9 + s.ph);
var alpha = fadeIn * fadeOut * pulse * 205;
p.fill(...(s.atA ? colors.accent2 : colors.accent1), alpha);
p.noStroke();
p.circle(s.x, s.y, 5.2);
return true;
});
// Update and draw orbiting particles
for (let i = particles.length - 1; i >= 0; i--) {
let pt = particles[i];
pt.age++;
let n = p.noise(pt.x * 0.0082 + time * 0.37, pt.y * 0.0082 + time * 0.30);
let ang = n * p.TWO_PI * 2;
pt.vx = p.lerp(pt.vx, Math.cos(ang) * 1.3, 0.075);
pt.vy = p.lerp(pt.vy, Math.sin(ang) * 1.3, 0.075);
pt.x += pt.vx;
pt.y += pt.vy;
if (pt.x < 100) pt.vx += 0.28;
if (pt.x > 300) pt.vx -= 0.28;
if (pt.y < 32) pt.vy += 0.28;
if (pt.y > 268) pt.vy -= 0.28;
pt.x = p.constrain(pt.x, 5, 395);
pt.y = p.constrain(pt.y, 5, 295);
let commitProb = 0.0007 + pt.age * 0.0000018;
if (p.random() < commitProb && settled.length < 42) {
let atA = pt.preferA;
let px = atA ? poleAx : poleBx;
let py = atA ? poleAy : poleBy;
let a2 = p.random(p.TWO_PI);
let r = p.random(20, 46);
settled.push({
x: px + Math.cos(a2) * r,
y: py + Math.sin(a2) * r,
atA: atA,
age: 0,
ph: p.random(p.TWO_PI)
});
particles[i] = newParticle();
continue;
}
let t = (pt.x - 100) / 200;
t = Math.max(0, Math.min(1, t));
let cr = p.lerp(colors.accent2[0], colors.accent1[0], t);
let cg = p.lerp(colors.accent2[1], colors.accent1[1], t);
let cb = p.lerp(colors.accent2[2], colors.accent1[2], t);
p.fill(cr, cg, cb, 85);
p.noStroke();
p.circle(pt.x, pt.y, 2.7);
}
p.fill(...colors.accent3, 82);
p.noStroke();
p.textAlign(p.CENTER);
p.textSize(9);
p.text('uncommitted — and settled', 200, 294);
};
};