Where ideas percolate and thoughts brew

The Coherence Trap

About This Sketch

Evidence streams in from the edges. Confirming ideas are captured and orbit the belief core. Challenging ideas are deflected — and each deflection makes the well slightly larger, slightly stronger. Watch the counters diverge.

Algorithm

A gravity-well simulation representing the coherence trap in belief formation. A central "belief core" exerts gravitational pull on incoming evidence particles. Two types of particles stream in from the canvas edges: confirming evidence (warm) and challenging evidence (muted). When a particle reaches the well boundary, confirming evidence is captured and begins to orbit; challenging evidence is deflected outward. Each rejection causes the well to pulse and grow slightly larger — making it harder for future challenging evidence to penetrate. Over time, the well grows denser as rejections accumulate, visually representing how coherence-seeking compounds: the stronger the worldview, the less new challenging evidence can reach it. This sketch accompanies the blog post "The Coherence Trap."

Pseudocode

SETUP:
  Initialize 400x300 canvas
  Set wellMass = 1.0, captured/rejected counters = 0

SPAWN (every 42 frames):
  Create particle at random point on circle of radius ~155 around center
  Aim loosely toward center with slight angular variance
  Mark as confirming (38% chance) or challenging (62% chance)

DRAW (every frame):
  Compute wellR = 28 + wellMass * 10 + pulseAmt * 12
  Render belief core as layered glow circles at center

  FOR each particle:
    IF active:
      Apply gravity toward center (strength proportional to wellMass)
      IF reached inner boundary (d < wellR * 0.78):
        IF confirming: transition to orbiting state, increment captured
        IF challenging: deflect outward, grow wellMass, pulse, increment rejected
    IF orbiting:
      Rotate around center, slowly spiral inward
    IF deflected:
      Continue outward with friction

  Display counters (integrated / rejected) and caption

Source Code

let sketch = function(p) {
    const W = 400, H = 300;
    const CX = W / 2, CY = H / 2;

    let wellMass = 1.0;
    let particles = [];
    let captured = 0, rejected = 0;
    let spawnTimer = 0;
    let pulseAmt = 0;

    function spawnParticle() {
        let angle = p.random(p.TWO_PI);
        let dist = 155 + p.random(25);
        let x = p.constrain(CX + Math.cos(angle) * dist, 6, W - 6);
        let y = p.constrain(CY + Math.sin(angle) * dist, 6, H - 6);
        let toCenter = Math.atan2(CY - y, CX - x) + p.random(-0.45, 0.45);
        let speed = p.random(1.1, 1.9);
        let isConfirming = p.random() < 0.38;
        return {
            x, y,
            vx: Math.cos(toCenter) * speed,
            vy: Math.sin(toCenter) * speed,
            isConfirming,
            state: 'active',
            orbitAngle: 0,
            orbitRadius: 0,
            age: 0
        };
    }

    p.setup = function() {
        p.createCanvas(W, H);
        p.randomSeed(42);
    };

    p.draw = function() {
        const colors = getThemeColors();
        p.background(...colors.bg);

        spawnTimer++;
        if (spawnTimer > 42) {
            particles.push(spawnParticle());
            spawnTimer = 0;
        }

        pulseAmt *= 0.88;
        let wellR = 28 + wellMass * 10 + pulseAmt * 12;

        p.noStroke();
        for (let r = wellR * 2.8; r > wellR * 0.4; r -= 10) {
            let a = p.map(r, wellR * 2.8, wellR * 0.4, 0, 45);
            p.fill(...colors.accent2, a);
            p.circle(CX, CY, r);
        }
        p.fill(...colors.accent2, 95);
        p.circle(CX, CY, wellR * 1.35);
        p.fill(...colors.accent2, 220);
        p.circle(CX, CY, 15);

        for (let i = particles.length - 1; i >= 0; i--) {
            let pt = particles[i];
            pt.age++;

            if (pt.state === 'active') {
                let dx = CX - pt.x;
                let dy = CY - pt.y;
                let d = Math.sqrt(dx * dx + dy * dy);
                let grav = (wellMass * 0.028) / Math.max(d * 0.032, 0.6);
                pt.vx += (dx / d) * grav;
                pt.vy += (dy / d) * grav;
                pt.vx *= 0.975;
                pt.vy *= 0.975;
                pt.x += pt.vx;
                pt.y += pt.vy;

                if (d < wellR * 0.78) {
                    if (pt.isConfirming) {
                        pt.state = 'orbiting';
                        pt.orbitAngle = Math.atan2(pt.y - CY, pt.x - CX);
                        pt.orbitRadius = wellR * (0.85 + p.random(0.65));
                        captured++;
                    } else {
                        pt.state = 'deflected';
                        let outAngle = Math.atan2(pt.y - CY, pt.x - CX);
                        pt.vx = Math.cos(outAngle) * 2.2 + p.random(-0.4, 0.4);
                        pt.vy = Math.sin(outAngle) * 2.2 + p.random(-0.4, 0.4);
                        wellMass = Math.min(5.5, wellMass + 0.18);
                        pulseAmt = 1.0;
                        rejected++;
                    }
                }
            } else if (pt.state === 'orbiting') {
                let spd = 0.013 / (pt.orbitRadius / 50);
                pt.orbitAngle += spd;
                pt.orbitRadius = Math.max(wellR * 0.52, pt.orbitRadius * 0.9985);
                pt.x = CX + Math.cos(pt.orbitAngle) * pt.orbitRadius;
                pt.y = CY + Math.sin(pt.orbitAngle) * pt.orbitRadius;
            } else {
                pt.x += pt.vx;
                pt.y += pt.vy;
                pt.vx *= 0.975;
                pt.vy *= 0.975;
            }

            let col = pt.isConfirming ? colors.accent1 : colors.accent3;
            let dim = pt.state === 'deflected';
            p.noStroke();
            p.fill(...col, dim ? 48 : 75);
            p.circle(pt.x, pt.y, 10);
            p.fill(...col, dim ? 75 : 200);
            p.circle(pt.x, pt.y, 4);

            if (pt.age > 520 || pt.x < -35 || pt.x > W + 35 || pt.y < -35 || pt.y > H + 35) {
                particles.splice(i, 1);
            }
        }

        p.noStroke();
        p.fill(...colors.accent2, 130);
        p.textAlign(p.CENTER);
        p.textSize(9);
        p.text('belief core', CX, CY + wellR + 17);

        p.textAlign(p.LEFT);
        p.textSize(8);
        p.fill(...colors.accent1, 115);
        p.text('integrated: ' + captured, 10, 14);
        p.fill(...colors.accent3, 115);
        p.text('rejected: ' + rejected, 10, 26);

        p.noStroke();
        p.fill(...colors.accent3, 50);
        p.textAlign(p.CENTER);
        p.textSize(8);
        p.text('each rejection deepens the well', W / 2, H - 5);
    };
};