Where ideas percolate and thoughts brew

The Legibility Trap

About This Sketch

Particles trace complex paths through a Perlin noise flow field — rich, continuous, impossible to fully capture. A grid of arrows periodically fades in, sampling the same field at discrete points and rendering it as simple directional markers. As the arrows grow prominent, the particles recede. When the arrows fade, the flow returns. The two representations breathe in opposition: the living knowledge and its legible approximation, each briefly dominant, neither complete.

Algorithm

Two representations of the same underlying system oscillate in visibility. Particles flow through a Perlin noise field, tracing complex, unpredictable paths that encode the full richness of the field. A sparse grid of arrows samples that same field at fixed points and displays simplified directional arrows. As the arrow grid fades in, the particles fade out — the documentation displacing the knowledge. When the arrows fade, the particles reassert themselves. This sketch accompanies the blog post "The Legibility Trap" and visualizes the gap between tacit expertise (the flowing particles) and its documented approximation (the arrow grid).

Pseudocode

SETUP:
  Initialize canvas (400x300)
  Seed 230 particles at random positions with zero velocity

DRAW (every frame):
  Get current theme colors
  Soft-fade background (trail effect)
  Advance time counter

  Compute arrowPhase = oscillating 0->1->0 (~18s period)

  FOR each particle:
    Sample Perlin noise at particle position + time offset
    Convert noise to angle; steer particle velocity toward that angle
    Move particle; wrap at canvas edges
    Respawn particle if age exceeds lifetime
    Draw particle with alpha scaled by arrowPhase (fades when arrows dominate)

  IF arrowPhase > threshold:
    FOR each grid cell (12x9 grid):
      Sample same noise field at cell center
      Draw directional arrow with alpha scaled by arrowPhase

  Draw caption text

Source Code

let sketch = function(p) {
    // Tacit knowledge vs. legibility
    // Particles flow through a rich Perlin noise field — complex, living, inarticulate
    // A sparse grid of arrows slowly fades in: the "documented" version
    // The arrows capture approximate direction but flatten all the variation
    // Then the arrows fade back out and the flow reasserts itself

    let particles = [];
    let time = 0;
    let cols = 12;
    let rows = 9;
    let cellW, cellH;

    p.setup = function() {
        p.createCanvas(400, 300);
        p.colorMode(p.RGB);
        cellW = 400 / cols;
        cellH = 300 / rows;

        for (let i = 0; i < 230; i++) {
            particles.push({
                x: p.random(400),
                y: p.random(300),
                vx: 0,
                vy: 0,
                age: p.random(260)
            });
        }
    };

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

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

        time += 0.007;

        let arrowPhase = (p.sin(time * 0.35) + 1) * 0.5;

        for (let pt of particles) {
            let noiseVal = p.noise(pt.x * 0.007 + time * 0.35, pt.y * 0.007 + time * 0.28);
            let angle = noiseVal * p.TWO_PI * 2;
            pt.vx = p.lerp(pt.vx, p.cos(angle) * 1.3, 0.08);
            pt.vy = p.lerp(pt.vy, p.sin(angle) * 1.3, 0.08);
            pt.x += pt.vx;
            pt.y += pt.vy;
            pt.age++;

            if (pt.x < 0) pt.x = 400;
            if (pt.x > 400) pt.x = 0;
            if (pt.y < 0) pt.y = 300;
            if (pt.y > 300) pt.y = 0;

            if (pt.age > 280) {
                pt.x = p.random(400);
                pt.y = p.random(300);
                pt.vx = 0;
                pt.vy = 0;
                pt.age = 0;
            }

            let life = p.min(pt.age / 40, (280 - pt.age) / 40, 1);
            let particleAlpha = life * p.map(arrowPhase, 0, 1, 145, 18);
            p.noStroke();
            p.fill(...colors.accent1, particleAlpha);
            p.circle(pt.x, pt.y, 2.3);
        }

        let arrowAlphaMax = arrowPhase * 210;
        if (arrowAlphaMax > 4) {
            for (let col = 0; col < cols; col++) {
                for (let row = 0; row < rows; row++) {
                    let cx = (col + 0.5) * cellW;
                    let cy = (row + 0.5) * cellH;

                    let noiseVal = p.noise(cx * 0.007 + time * 0.35, cy * 0.007 + time * 0.28);
                    let angle = noiseVal * p.TWO_PI * 2;

                    let len = 9;
                    let ex = cx + p.cos(angle) * len;
                    let ey = cy + p.sin(angle) * len;
                    let sx = cx - p.cos(angle) * len * 0.4;
                    let sy = cy - p.sin(angle) * len * 0.4;

                    p.stroke(...colors.accent2, arrowAlphaMax);
                    p.strokeWeight(1.3);
                    p.line(sx, sy, ex, ey);

                    let hLen = 3.5;
                    let hAng = 0.55;
                    p.line(ex, ey,
                        ex - hLen * p.cos(angle - hAng),
                        ey - hLen * p.sin(angle - hAng));
                    p.line(ex, ey,
                        ex - hLen * p.cos(angle + hAng),
                        ey - hLen * p.sin(angle + hAng));
                }
            }
        }

        p.noStroke();
        p.fill(...colors.accent3, 90);
        p.textAlign(p.CENTER);
        p.textSize(9);
        p.text('the flow — and its documentation', 200, 294);
    };
};