Where ideas percolate and thoughts brew

Reality Is a Draft

About This Sketch

Particles drift through the canvas, each trailing a ghost of where the brain predicts it will be. The faint dots lead; the solid dots follow. Lines between them glow brighter when prediction and reality diverge. A visualization of predictive processing — the mechanism by which you experience not the world, but your model of it.

Algorithm

Each particle moves with a velocity that randomly drifts each frame (simulating an unpredictable world). A "predicted" position is maintained for every particle, extrapolated from current position and velocity with a lookahead factor. The predicted position smoothly tracks toward the extrapolated target, mimicking how the brain updates its model incrementally. The gap between actual and predicted position is the prediction error — visualized as a connecting line whose opacity scales with error magnitude. Ghost dots mark predictions; solid dots mark reality.

Pseudocode

SETUP:
  Create 28 particles at random positions with random velocities
  Initialize predicted positions equal to starting positions

DRAW each frame:
  Render semi-transparent background overlay (creates motion trail)
  For each particle:
    Add small random noise to velocity (world uncertainty)
    Clamp velocity to maximum speed
    Move particle by its velocity
    Compute target prediction = position + velocity * 12 (lookahead)
    Lerp predicted position toward target at rate 0.18 (gradual update)
    Wrap particle and prediction around canvas edges
    Compute error = distance(actual, predicted)
    Draw connecting line with opacity proportional to error
    Draw predicted position as faint ghost dot (larger)
    Draw actual position as solid dot (smaller)

Source Code

let sketch = function(p) {
    let particles = [];
    const NUM_PARTICLES = 28;

    function makeParticle() {
        return {
            x: p.random(p.width),
            y: p.random(p.height),
            vx: p.random(-1.2, 1.2),
            vy: p.random(-1.2, 1.2),
            px: 0,
            py: 0,
            age: p.random(0, 100)
        };
    }

    p.setup = function() {
        p.createCanvas(400, 300);
        p.colorMode(p.RGB);
        for (let i = 0; i < NUM_PARTICLES; i++) {
            let part = makeParticle();
            part.px = part.x;
            part.py = part.y;
            particles.push(part);
        }
    };

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

        p.fill(...colors.bg, 45);
        p.noStroke();
        p.rect(0, 0, p.width, p.height);

        for (let part of particles) {
            part.age++;

            part.vx += p.random(-0.08, 0.08);
            part.vy += p.random(-0.08, 0.08);

            let speed = p.sqrt(part.vx * part.vx + part.vy * part.vy);
            if (speed > 1.8) {
                part.vx = (part.vx / speed) * 1.8;
                part.vy = (part.vy / speed) * 1.8;
            }

            part.x += part.vx;
            part.y += part.vy;

            let lookahead = 12;
            let targetPx = part.x + part.vx * lookahead;
            let targetPy = part.y + part.vy * lookahead;

            part.px += (targetPx - part.px) * 0.18;
            part.py += (targetPy - part.py) * 0.18;

            if (part.x > p.width)  { part.x -= p.width;  part.px -= p.width; }
            if (part.x < 0)        { part.x += p.width;  part.px += p.width; }
            if (part.y > p.height) { part.y -= p.height; part.py -= p.height; }
            if (part.y < 0)        { part.y += p.height; part.py += p.height; }

            let err = p.dist(part.x, part.y, part.px, part.py);

            let lineAlpha = p.map(err, 0, 50, 15, 130);
            p.stroke(...colors.accent2, lineAlpha);
            p.strokeWeight(0.6);
            p.line(part.x, part.y, part.px, part.py);

            p.noStroke();
            let ghostAlpha = p.map(err, 0, 50, 60, 130);
            p.fill(...colors.accent1, ghostAlpha);
            p.ellipse(part.px, part.py, 7, 7);

            p.fill(...colors.accent3, 200);
            p.ellipse(part.x, part.y, 4, 4);
        }
    };
};