Where ideas percolate and thoughts brew

The Intuition Trap

About This Sketch

When does trusting your gut work? This sketch visualizes the answer: only when your intuition has been shaped by real feedback from a stable environment. The left panel shows particles with accurate prediction (their "gut" points toward the actual attractor, and feedback loops close). The right shows the same particles with equally confident but uncalibrated predictions โ€” pointing in directions unrelated to their actual movement. Same confidence, different underlying structure.

Algorithm

Two panels showing particle systems in contrasting environments. Left (high-validity): particles are attracted to a slowly drifting target. Their "prediction ghosts" point accurately toward the attractor, and feedback arcs close the loop when particles approach it โ€” representing genuine pattern recognition built from real signal. Right (low-validity): particles drift via noise fields. Their prediction ghosts point confidently but in directions uncorrelated with actual motion. No feedback arc ever closes โ€” representing intuition trained on noise.

Pseudocode

SETUP:
  Create N particles in each panel
  Assign each random position, velocity, noise offset

LEFT PANEL each frame:
  Move attractor slowly (sine wave drift)
  Pull each particle toward attractor with dampening
  Draw trail behind particle
  Draw prediction ghost ahead in attractor direction
  If particle near attractor, draw closing feedback arc

RIGHT PANEL each frame:
  Move particle along perlin noise field
  Draw trail behind particle
  Draw prediction ghost in DIFFERENT noise direction
  No feedback arc drawn (loop never closes)

BOTH PANELS:
  Draw panel labels and divider
  Caption: "same confidence ยท different calibration"

Source Code

let sketch = function(p) {
    const W = 400, H = 300;
    const HALF = W / 2;
    const N = 18;

    let highParticles = [];
    let lowParticles = [];
    let time = 0;

    function makeParticle(px, py) {
        return {
            x: px, y: py,
            vx: p.random(-0.4, 0.4),
            vy: p.random(-0.4, 0.4),
            phase: p.random(p.TWO_PI),
            noiseOffset: p.random(1000),
            trail: []
        };
    }

    p.setup = function() {
        p.createCanvas(W, H);
        p.randomSeed(99);
        for (let i = 0; i < N; i++) {
            highParticles.push(makeParticle(p.random(16, HALF - 16), p.random(30, H - 20)));
            lowParticles.push(makeParticle(p.random(HALF + 16, W - 16), p.random(30, H - 20)));
        }
    };

    function drawFeedbackArc(colors, x1, y1, x2, y2, alpha) {
        p.noFill();
        p.stroke(...colors.accent1, alpha);
        p.strokeWeight(0.8);
        let mx = (x1 + x2) / 2;
        let my = Math.min(y1, y2) - 18;
        p.beginShape();
        p.vertex(x1, y1);
        p.quadraticVertex(mx, my, x2, y2);
        p.endShape();
    }

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

        p.noStroke();
        p.textAlign(p.CENTER);
        p.textSize(9);
        p.fill(...colors.accent2, 170);
        p.text('high-validity domain', HALF / 2, 16);
        p.fill(...colors.accent3, 120);
        p.text('low-validity domain', HALF + HALF / 2, 16);

        p.stroke(...colors.accent3, 20);
        p.strokeWeight(0.7);
        p.line(HALF, 22, HALF, H - 8);

        let ax = HALF / 2 + 28 * Math.cos(time * 0.31);
        let ay = H / 2 + 22 * Math.sin(time * 0.22);

        p.noStroke();
        p.fill(...colors.accent2, 30);
        p.circle(ax, ay, 28);
        p.fill(...colors.accent2, 80);
        p.circle(ax, ay, 10);

        for (let pt of highParticles) {
            let dx = ax - pt.x;
            let dy = ay - pt.y;
            let d = Math.sqrt(dx * dx + dy * dy);
            let pull = 0.09 / Math.max(d * 0.04, 1);
            pt.vx += dx * pull + p.random(-0.04, 0.04);
            pt.vy += dy * pull + p.random(-0.04, 0.04);
            pt.vx *= 0.88;
            pt.vy *= 0.88;
            pt.x += pt.vx;
            pt.y += pt.vy;
            pt.x = p.constrain(pt.x, 8, HALF - 8);
            pt.y = p.constrain(pt.y, 26, H - 8);

            pt.trail.push({ x: pt.x, y: pt.y });
            if (pt.trail.length > 18) pt.trail.shift();

            p.noStroke();
            for (let i = 0; i < pt.trail.length - 1; i++) {
                let a = (i / pt.trail.length) * 55;
                p.fill(...colors.accent1, a);
                p.circle(pt.trail[i].x, pt.trail[i].y, 2.5);
            }

            let predX = p.constrain(pt.x + dx * 0.25, 10, HALF - 10);
            let predY = p.constrain(pt.y + dy * 0.25, 26, H - 8);
            p.fill(...colors.accent1, 38);
            p.circle(predX, predY, 8);

            if (d < 55) {
                drawFeedbackArc(colors, pt.x, pt.y, ax, ay, 28 * (1 - d / 55));
            }

            p.noStroke();
            p.fill(...colors.accent1, 75);
            p.circle(pt.x, pt.y, 10);
            p.fill(...colors.accent1, 200);
            p.circle(pt.x, pt.y, 5);
        }

        for (let pt of lowParticles) {
            let noiseAngle = p.noise(pt.noiseOffset + time * 0.4) * p.TWO_PI * 3;
            pt.vx += Math.cos(noiseAngle) * 0.22;
            pt.vy += Math.sin(noiseAngle) * 0.22;
            pt.vx *= 0.86;
            pt.vy *= 0.86;
            pt.x += pt.vx;
            pt.y += pt.vy;
            pt.x = p.constrain(pt.x, HALF + 8, W - 8);
            pt.y = p.constrain(pt.y, 26, H - 8);

            pt.trail.push({ x: pt.x, y: pt.y });
            if (pt.trail.length > 18) pt.trail.shift();

            p.noStroke();
            for (let i = 0; i < pt.trail.length - 1; i++) {
                let a = (i / pt.trail.length) * 45;
                p.fill(...colors.accent3, a);
                p.circle(pt.trail[i].x, pt.trail[i].y, 2.5);
            }

            let predAngle = p.noise(pt.noiseOffset + 500 + time * 0.15) * p.TWO_PI * 3;
            let predX = p.constrain(pt.x + Math.cos(predAngle) * 24, HALF + 10, W - 10);
            let predY = p.constrain(pt.y + Math.sin(predAngle) * 24, 26, H - 8);

            p.stroke(...colors.accent3, 55);
            p.strokeWeight(0.9);
            p.line(pt.x, pt.y, predX, predY);
            p.noStroke();
            p.fill(...colors.accent3, 50);
            p.circle(predX, predY, 7);

            p.noStroke();
            p.fill(...colors.accent3, 65);
            p.circle(pt.x, pt.y, 10);
            p.fill(...colors.accent3, 190);
            p.circle(pt.x, pt.y, 5);
        }

        p.noStroke();
        p.fill(...colors.accent3, 55);
        p.textAlign(p.CENTER);
        p.textSize(8);
        p.text('same confidence \u00b7 different calibration', W / 2, H - 4);
    };
};