Where ideas percolate and thoughts brew

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);
    };
};