You Don't Have a Procrastination Problem
About This Sketch
Seven particles orbit a central task node, each slowly spiraling inward—but periodically flinching outward in brief avoidance bursts. Colors shift from muted to warm as each particle closes in. When a particle finally reaches the task, it completes with an expanding ripple. A visualization of the approach-avoidance loop at the heart of procrastination, accompanying the post "You Don't Have a Procrastination Problem."
Algorithm
Seven particles orbit a central "task" node at varying radii. Each particle slowly spirals
inward through orbital decay, representing the gradual approach toward action. Periodically,
each particle experiences a "flinch"—a brief outward burst that pushes it away from the
task—visualizing the avoidance impulse. As a particle draws closer to the task, its color
warms from muted gray-brown toward bright amber. When it finally reaches the center, it
completes with an expanding ripple. After all particles complete, the scene resets.
Color encodes emotional distance: cool and muted when avoidance is high, warm and vivid
when the particle is close to breaking through.
Pseudocode
SETUP:
Initialize canvas (400x300)
Create 7 particles at evenly distributed angles, each at ~85px orbit radius
Each particle has: angle, radius, speed, direction, decay rate, flinch cooldown
DRAW (every frame):
Get current theme colors
Clear background
Draw central task node with pulsing glow effect
Label "task" beneath node
FOR each particle:
IF done: animate completion ripple, skip
Decrement flinch cooldown
IF cooldown expired AND particle is far enough: trigger flinch burst
IF flinching: increase radius (avoidance)
ELSE: decay radius (spiral inward)
Advance angle by speed * direction
IF radius <= task radius: mark done, trigger ripple
Compute progress (0 = far, 1 = close)
Interpolate color from muted to warm based on progress
Draw faint orbit ring
Draw tail segment (shows direction)
Draw particle dot (slightly larger when close)
IF all particles done: wait 100 frames, then reinitialize
Source Code
let sketch = function(p) {
let particles = [];
let cx = 200, cy = 150;
let taskR = 14;
let timer = 0;
let completedCount = 0;
let resetTimer = 0;
function makeParticle(i, n) {
let angle = (i / n) * p.TWO_PI + p.random(-0.4, 0.4);
let orbitR = 85 + p.random(-25, 25);
return {
angle: angle,
r: orbitR,
startR: orbitR,
speed: 0.016 + p.random(0, 0.012),
dir: (p.random() > 0.5 ? 1 : -1),
decay: 0.00035 + p.random(0, 0.00025),
done: false,
flinch: 0,
flinchCooldown: p.floor(p.random(90, 300)),
alpha: 255
};
}
function init() {
particles = [];
completedCount = 0;
resetTimer = 0;
let n = 7;
for (let i = 0; i < n; i++) {
particles.push(makeParticle(i, n));
}
}
p.setup = function() {
p.createCanvas(400, 300);
p.colorMode(p.RGB);
init();
};
p.draw = function() {
const colors = getThemeColors();
p.background(...colors.bg);
timer++;
let pulse = 0.5 + 0.5 * Math.sin(timer * 0.04);
p.noStroke();
for (let i = 4; i >= 1; i--) {
let a = 15 + i * 12 * pulse;
p.fill(colors.accent2[0], colors.accent2[1], colors.accent2[2], a);
p.circle(cx, cy, (taskR + i * 7) * 2);
}
p.fill(...colors.accent2);
p.circle(cx, cy, taskR * 2);
p.fill(colors.accent2[0], colors.accent2[1], colors.accent2[2], 120);
p.textSize(9);
p.textAlign(p.CENTER);
p.noStroke();
p.text('task', cx, cy + taskR + 14);
let allDone = true;
for (let pt of particles) {
if (pt.done) {
if (pt.alpha > 0) {
pt.alpha -= 4;
p.noFill();
p.stroke(colors.accent1[0], colors.accent1[1], colors.accent1[2], pt.alpha);
p.strokeWeight(1.5);
let rippleR = (255 - pt.alpha) * 0.4 + taskR;
p.circle(cx, cy, rippleR * 2);
}
continue;
}
allDone = false;
pt.flinchCooldown--;
if (pt.flinchCooldown <= 0 && pt.r > taskR + 20) {
pt.flinch = 18 + p.random(8, 16);
pt.flinchCooldown = p.floor(p.random(120, 360));
}
if (pt.flinch > 0) {
pt.r += 1.2;
pt.flinch--;
} else {
pt.r = Math.max(taskR + 2, pt.r - pt.decay * pt.r);
}
pt.angle += pt.speed * pt.dir;
if (pt.r <= taskR + 3) {
pt.done = true;
pt.alpha = 200;
completedCount++;
continue;
}
let x = cx + Math.cos(pt.angle) * pt.r;
let y = cy + Math.sin(pt.angle) * pt.r;
let progress = 1 - (pt.r - taskR) / (pt.startR - taskR);
progress = Math.max(0, Math.min(1, progress));
let rc = p.lerp(colors.accent3[0], colors.accent1[0], progress);
let gc = p.lerp(colors.accent3[1], colors.accent1[1], progress);
let bc = p.lerp(colors.accent3[2], colors.accent1[2], progress);
p.noFill();
p.stroke(rc, gc, bc, 18);
p.strokeWeight(1);
p.circle(cx, cy, pt.r * 2);
let tailAngle = pt.angle - pt.dir * 0.18;
let tx = cx + Math.cos(tailAngle) * pt.r;
let ty = cy + Math.sin(tailAngle) * pt.r;
p.stroke(rc, gc, bc, 90);
p.strokeWeight(2);
p.line(x, y, tx, ty);
p.noStroke();
p.fill(rc, gc, bc, 210);
p.circle(x, y, 7 + progress * 2);
}
if (allDone) {
resetTimer++;
if (resetTimer > 100) {
init();
}
}
};
};