Where ideas percolate and thoughts brew

The Constraint You Won't Name

About This Sketch

Particles flowing left to right must pass through a single narrow bottleneck at center. Most accumulate and slow before itβ€”the whole system's throughput determined by this one point. When the bottleneck briefly opens, everything moves freely. When it closes again, backup resumes immediately.

Visualizing Goldratt's Theory of Constraints: in any system, one binding constraint determines total output. Improving non-constraints just creates more work that piles up at the same narrow point.

Algorithm

A flow visualization showing particles moving from left to right through a channel that narrows to a tight bottleneck at the center. Particles accumulate and slow before the bottleneck, squeeze through at reduced speed, then accelerate freely afterward. The bottleneck periodically widens (representing the brief moments when the true constraint is addressed), and the whole system flows freelyβ€”then narrows again. Color encodes position relative to constraint: particles waiting behind the bottleneck appear in a deeper tone; particles past it move in the lighter accent color. This sketch accompanies the blog post "The Constraint You Won't Name" and visualizes Goldratt's Theory of Constraints applied to human performance: one binding limit determines total throughput, and improving anything else just creates backup at the same point.

Pseudocode

SETUP:
  Initialize canvas (400x300)
  Set bottleneck to closed state with timer
  Seed 55 particles at random positions across canvas

EACH FRAME:
  Apply translucent background for trail effect
  Advance time counter

  MANAGE BOTTLENECK CYCLE:
    Decrement timer
    If closed timer expires β†’ open bottleneck (80 frames)
    If open timer expires β†’ close bottleneck (260+ frames)
    Lerp current neck width toward target (smooth transition)

  DRAW CHANNEL WALLS:
    Bezier curves funnel from full width β†’ bottleneck β†’ full width
    Glow effect near closed bottleneck

  SPAWN new particle every 5 frames if under limit

  FOR EACH PARTICLE:
    Increment age, fade in alpha
    Compute channel bounds at particle's x position
    Push away from walls with margin
    If approaching bottleneck from left: slow down based on crowding
    If past bottleneck: accelerate to exit speed
    Apply Perlin noise perturbation for organic drift
    Constrain velocities
    Move particle
    Remove if off-canvas or too old
    Color blend: waiting (deep) β†’ passed (light)

  DRAW label

Source Code

let sketch = function(p) {
    // Flow network: particles flow from left to right through a wide system
    // but must all pass through one narrow bottleneck in the center
    // Particles pile up behind the bottleneck, slow through, then disperse
    // Occasionally the bottleneck briefly widens and everything flows freely
    // Represents: one constraint limits the whole system; improving anything else just creates backup

    let particles = [];
    let time = 0;
    let bottleneckOpen = 0;
    let openTimer = 0;
    let closedTimer = 0;

    const BOTTLENECK_X = 200;
    const NECK_W = 18;
    const NECK_W_OPEN = 90;
    const TOP_WALL = 40;
    const BOT_WALL = 260;

    function spawnParticle() {
        return {
            x: p.random(10, 60),
            y: p.random(TOP_WALL + 5, BOT_WALL - 5),
            vx: p.random(0.6, 1.3),
            vy: p.random(-0.25, 0.25),
            size: p.random(2.4, 3.8),
            age: 0,
            alpha: 0,
            passed: false
        };
    }

    p.setup = function() {
        p.createCanvas(400, 300);
        p.colorMode(p.RGB);
        closedTimer = 200;
        p._neckW = NECK_W;
        for (let i = 0; i < 55; i++) {
            let pt = spawnParticle();
            pt.x = p.random(10, 370);
            pt.age = p.random(60);
            particles.push(pt);
        }
    };

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

        p.noStroke();
        p.fill(...colors.bg, 42);
        p.rect(0, 0, 400, 300);

        time += 0.013;

        if (bottleneckOpen === 0) {
            closedTimer--;
            if (closedTimer <= 0) {
                bottleneckOpen = 1;
                openTimer = 80;
            }
        } else {
            openTimer--;
            if (openTimer <= 0) {
                bottleneckOpen = 0;
                closedTimer = 260 + p.random(80);
            }
        }

        let targetW = bottleneckOpen === 1 ? NECK_W_OPEN : NECK_W;
        p._neckW = p.lerp(p._neckW, targetW, 0.04);
        let neckW = p._neckW;

        let wallColorA = [...colors.accent3, 55];
        p.stroke(...wallColorA);
        p.strokeWeight(1.5);
        p.noFill();

        p.beginShape();
        p.vertex(0, TOP_WALL);
        p.bezierVertex(120, TOP_WALL, 160, 150 - neckW, BOTTLENECK_X, 150 - neckW);
        p.bezierVertex(240, 150 - neckW, 280, TOP_WALL, 400, TOP_WALL);
        p.endShape();

        p.beginShape();
        p.vertex(0, BOT_WALL);
        p.bezierVertex(120, BOT_WALL, 160, 150 + neckW, BOTTLENECK_X, 150 + neckW);
        p.bezierVertex(240, 150 + neckW, 280, BOT_WALL, 400, BOT_WALL);
        p.endShape();

        if (bottleneckOpen === 0 && neckW < NECK_W + 15) {
            let glowA = p.map(neckW, NECK_W, NECK_W + 15, 60, 0);
            p.noStroke();
            for (let r = neckW + 20; r > neckW; r -= 4) {
                p.fill(...colors.accent2, glowA * (1 - (r - neckW) / 20));
                p.rect(BOTTLENECK_X - r, 150 - r * 0.4, r * 2, r * 0.8, 4);
            }
        }

        if (p.frameCount % 5 === 0 && particles.length < 110) {
            particles.push(spawnParticle());
        }

        for (let i = particles.length - 1; i >= 0; i--) {
            let pt = particles[i];
            pt.age++;
            pt.alpha = Math.min(pt.alpha + 8, 180);

            function getChannelBounds(x) {
                let topY, botY;
                if (x < BOTTLENECK_X) {
                    let s = x / BOTTLENECK_X;
                    topY = p.lerp(TOP_WALL, 150 - neckW, s * s);
                    botY = p.lerp(BOT_WALL, 150 + neckW, s * s);
                } else {
                    let s = (x - BOTTLENECK_X) / (400 - BOTTLENECK_X);
                    topY = p.lerp(150 - neckW, TOP_WALL, s * s);
                    botY = p.lerp(150 + neckW, BOT_WALL, s * s);
                }
                return { topY, botY };
            }

            let bounds = getChannelBounds(pt.x);
            let margin = pt.size + 2;
            if (pt.y < bounds.topY + margin) pt.vy += 0.3;
            if (pt.y > bounds.botY - margin) pt.vy -= 0.3;

            let distToNeck = Math.abs(pt.x - BOTTLENECK_X);
            if (!pt.passed && distToNeck < 80 && pt.x < BOTTLENECK_X) {
                let channelHalfH = (bounds.botY - bounds.topY) / 2;
                let crowding = 1 - Math.min(1, channelHalfH / 80);
                pt.vx = p.lerp(pt.vx, 0.25, 0.06 + crowding * 0.08);
            } else {
                pt.vx = p.lerp(pt.vx, 1.4, 0.04);
            }

            if (pt.x >= BOTTLENECK_X) pt.passed = true;
            if (pt.passed) pt.vx = p.lerp(pt.vx, 2.0, 0.05);

            let n = p.noise(pt.x * 0.007 + time * 0.2, pt.y * 0.007 + time * 0.15);
            pt.vy = p.lerp(pt.vy, (n - 0.5) * 1.2, 0.05);

            pt.vx = p.constrain(pt.vx, 0.1, 3.5);
            pt.vy = p.constrain(pt.vy, -1.8, 1.8);

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

            if (pt.x > 410 || pt.age > 600) {
                particles.splice(i, 1);
                continue;
            }

            let t = pt.passed ? 1 : p.constrain((BOTTLENECK_X - pt.x) / 120, 0, 1);
            let cr = p.lerp(colors.accent1[0], colors.accent2[0], t);
            let cg = p.lerp(colors.accent1[1], colors.accent2[1], t);
            let cb = p.lerp(colors.accent1[2], colors.accent2[2], t);

            p.noStroke();
            p.fill(cr, cg, cb, pt.alpha);
            p.circle(pt.x, pt.y, pt.size * 2);
        }

        p.noStroke();
        p.fill(...colors.accent3, 80);
        p.textAlign(p.CENTER);
        p.textSize(9);
        p.text('the system β€” and its constraint', 200, 290);
    };
};