Where ideas percolate and thoughts brew

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