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