Where ideas percolate and thoughts brew

The Commitment Trap

About This Sketch

Two particles start from the same point. One stays free to update โ€” it quickly finds and orbits the optimum. The other accumulates commitment inertia over time, making course correction progressively harder and harder. A visualization of why the cost of updating a committed direction compounds the longer you wait. Accompanies "The Commitment Trap."

Algorithm

Two particles start at the same position with the same initial velocity, both pulled toward a central "optimum" point. The free particle updates quickly: strong pull, high damping, rapid convergence to the optimum. The committed particle accumulates "commitment inertia" over time โ€” its pull toward the optimum is divided by its growing mass, and its damping decreases. The longer it has been committed, the harder course correction becomes. A growing circle size on the committed particle visualizes accumulated inertia. A progress bar shows commitment mass. Trails show diverging paths from the same starting point. This accompanies "The Commitment Trap."

Pseudocode

SETUP:
  Initialize two particles at same position with same velocity
  committed.mass = 1.0 (will grow)

DRAW (every frame):
  Get theme colors
  Clear background
  committed.mass = min(6.0, 1.0 + frame * 0.008)

  FREE PARTICLE:
    Pull toward optimum with full strength
    Damp velocity strongly
    Update position

  COMMITTED PARTICLE:
    Pull toward optimum divided by mass (harder to update)
    Damp less as mass grows (inertia)
    Update position

  Draw optimum glow and center point
  Draw particle trails (fading)
  Draw particles (committed grows larger over time)
  Draw commitment inertia bar
  Draw legend and caption

Source Code

let sketch = function(p) {
    // The Commitment Trap: a particle locked to a trajectory by "commitment gravity."
    // A second free particle is shown side-by-side: same setup, but no commitment
    // mass โ€” it quickly finds and orbits the optimum.
    // Caption: "inertia compounds ยท late updates cost more"

    const W = 400, H = 300;
    const OPT_X = W / 2, OPT_Y = H / 2;
    const OPT_R = 10;

    let committed = {
        x: 60, y: 60, vx: 1.4, vy: 0.7,
        mass: 1.0,
        trail: []
    };

    let free = {
        x: 60, y: 60, vx: 1.4, vy: 0.7,
        trail: []
    };

    let frame = 0;
    const MAX_TRAIL = 55;

    p.setup = function() {
        p.createCanvas(W, H);
        p.randomSeed(3);
    };

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

        committed.mass = p.min(6.0, 1.0 + frame * 0.008);

        {
            let dx = OPT_X - free.x;
            let dy = OPT_Y - free.y;
            let d = Math.sqrt(dx * dx + dy * dy) + 1;
            let pull = 0.18;
            free.vx += (dx / d) * pull;
            free.vy += (dy / d) * pull;
            free.vx *= 0.91;
            free.vy *= 0.91;
            free.x += free.vx;
            free.y += free.vy;
            if (free.x < 5) { free.x = 5; free.vx *= -0.6; }
            if (free.x > W - 5) { free.x = W - 5; free.vx *= -0.6; }
            if (free.y < 5) { free.y = 5; free.vy *= -0.6; }
            if (free.y > H - 5) { free.y = H - 5; free.vy *= -0.6; }
            free.trail.push({ x: free.x, y: free.y });
            if (free.trail.length > MAX_TRAIL) free.trail.shift();
        }

        {
            let dx = OPT_X - committed.x;
            let dy = OPT_Y - committed.y;
            let d = Math.sqrt(dx * dx + dy * dy) + 1;
            let pull = 0.18 / committed.mass;
            committed.vx += (dx / d) * pull;
            committed.vy += (dy / d) * pull;
            let damp = p.lerp(0.91, 0.97, (committed.mass - 1) / 5);
            committed.vx *= damp;
            committed.vy *= damp;
            committed.x += committed.vx;
            committed.y += committed.vy;
            if (committed.x < 5) { committed.x = 5; committed.vx *= -0.6; }
            if (committed.x > W - 5) { committed.x = W - 5; committed.vx *= -0.6; }
            if (committed.y < 5) { committed.y = 5; committed.vy *= -0.6; }
            if (committed.y > H - 5) { committed.y = H - 5; committed.vy *= -0.6; }
            committed.trail.push({ x: committed.x, y: committed.y });
            if (committed.trail.length > MAX_TRAIL) committed.trail.shift();
        }

        for (let r = 40; r > 0; r -= 6) {
            let a = p.map(r, 0, 40, 30, 0);
            p.noStroke();
            p.fill(...colors.accent1, a);
            p.circle(OPT_X, OPT_Y, r * 2);
        }
        p.fill(...colors.accent1);
        p.noStroke();
        p.circle(OPT_X, OPT_Y, OPT_R * 2);
        p.fill(...colors.bg);
        p.circle(OPT_X, OPT_Y, OPT_R);
        p.textAlign(p.CENTER);
        p.textSize(8);
        p.fill(...colors.accent1, 130);
        p.noStroke();
        p.text('optimum', OPT_X, OPT_Y + OPT_R + 10);

        p.noFill();
        for (let i = 1; i < free.trail.length; i++) {
            let t = i / free.trail.length;
            let a = t * 160;
            p.stroke(...colors.accent2, a);
            p.strokeWeight(1.5);
            p.line(free.trail[i-1].x, free.trail[i-1].y, free.trail[i].x, free.trail[i].y);
        }
        for (let i = 1; i < committed.trail.length; i++) {
            let t = i / committed.trail.length;
            let a = t * 120;
            p.stroke(...colors.accent3, a);
            p.strokeWeight(1.5);
            p.line(committed.trail[i-1].x, committed.trail[i-1].y, committed.trail[i].x, committed.trail[i].y);
        }

        p.noStroke();
        p.fill(...colors.accent2, 35);
        p.circle(free.x, free.y, 16);
        p.fill(...colors.accent2, 230);
        p.circle(free.x, free.y, 8);

        let cSize = p.map(committed.mass, 1, 6, 8, 18);
        p.fill(...colors.accent3, 30);
        p.circle(committed.x, committed.y, cSize + 10);
        p.fill(...colors.accent3, 185);
        p.circle(committed.x, committed.y, cSize);

        let barX = 10, barY = 12, barW = 90, barH = 7;
        p.noStroke();
        p.fill(...colors.accent3, 28);
        p.rect(barX, barY, barW, barH, 2);
        let massFraction = (committed.mass - 1) / 5;
        p.fill(...colors.accent3, 155);
        p.rect(barX, barY, barW * massFraction, barH, 2);
        p.fill(...colors.accent3, 130);
        p.textSize(8);
        p.textAlign(p.LEFT);
        p.text('commitment inertia', barX, barY - 2);

        p.textSize(8);
        p.fill(...colors.accent2, 190);
        p.circle(W - 110, 16, 6);
        p.fill(...colors.accent2, 160);
        p.textAlign(p.LEFT);
        p.text('free to update', W - 102, 20);
        p.fill(...colors.accent3, 160);
        p.circle(W - 110, 30, 8);
        p.fill(...colors.accent3, 130);
        p.text('committed', W - 102, 34);

        p.noStroke();
        p.fill(...colors.accent3, 50);
        p.textAlign(p.CENTER);
        p.textSize(8);
        p.text('inertia compounds \u00b7 late updates cost more', W / 2, H - 5);
    };
};