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