The Friendship Infrastructure
About This Sketch
Nodes drift across the canvas — some anchored in clusters, some scattered alone. When nodes are near each other, glowing connections appear between them. The clustered nodes, kept close by invisible social infrastructure, burn bright with connection. The isolated nodes drift mostly dark.
This sketch visualizes the thesis from "The Adult Friendship Crisis Is an Infrastructure Problem": friendship forms through proximity and repetition, not through effort and intention. The structure of your environment determines whether connection happens easily or not at all.
Algorithm
Nodes represent people scattered across a social landscape. Each frame, every node drifts organically via a Perlin noise flow field.
Two clusters of nodes are loosely anchored to invisible center points — representing communities with good social infrastructure (walkable neighborhoods, recurring activities, third places). These nodes stay near each other, forming dense, glowing connection networks.
A second group of nodes has no anchor and drifts freely across the canvas, representing people in social environments without structural support for friendship.
When any two nodes come within connection range, a glowing line forms between them. Connection strength — and therefore brightness and thickness — scales with proximity. Nodes with many connections glow brighter and appear larger.
The result: clustered, anchored nodes are perpetually lit with connections; isolated, unanchored nodes drift through the canvas mostly dark.
This sketch accompanies "The Adult Friendship Crisis Is an Infrastructure Problem" and visualizes the thesis that proximity creates friendship more reliably than intention does.
Pseudocode
SETUP:
Create two cluster anchors at left-center and right-center
Spawn 11 nodes near each anchor (with anchor reference)
Spawn 14 unanchored nodes at random positions
DRAW (every frame):
Get current theme colors
Fade background slightly (trail effect)
Increment time counter
FOR each node:
Sample Perlin noise at node position + time offset
Map noise to motion angle, lerp velocity toward it
IF node has anchor: add spring force toward anchor center
Move node by velocity, constrain to canvas
FOR each pair of nodes:
Compute distance
IF distance < CONNECTION_DIST:
Draw glowing line with opacity/weight scaled to proximity
FOR each node:
Count nearby nodes (connections)
Map connection count to brightness and size
Draw node with pulsing alpha and warm color
(well-connected nodes glow warm amber; isolated nodes appear dim)
Draw caption text
Source Code
let sketch = function(p) {
// Nodes (people) drift around the canvas.
// When two nodes are within range, a glowing connection forms between them.
// Some nodes are loosely anchored to cluster centers (infrastructure communities)
// and stay close together, forming dense connection networks.
// Isolated nodes drift alone with no connections.
// Represents: proximity creates friendship; structure does what effort cannot.
let nodes = [];
let time = 0;
const CONNECTION_DIST = 62;
function makeNode(x, y, anchorX, anchorY) {
return {
x: x,
y: y,
vx: p.random(-0.5, 0.5),
vy: p.random(-0.5, 0.5),
anchorX: anchorX,
anchorY: anchorY,
size: p.random(2.8, 4.4),
ph: p.random(p.TWO_PI)
};
}
p.setup = function() {
p.createCanvas(400, 300);
p.colorMode(p.RGB);
// Two infrastructure clusters: left and right
let clusters = [
{ x: 105, y: 150 },
{ x: 295, y: 150 }
];
for (let c of clusters) {
for (let i = 0; i < 11; i++) {
let ang = p.random(p.TWO_PI);
let r = p.random(18, 52);
nodes.push(makeNode(
c.x + Math.cos(ang) * r,
c.y + Math.sin(ang) * r,
c.x, c.y
));
}
}
// Scattered isolated nodes (no anchor)
for (let i = 0; i < 14; i++) {
nodes.push(makeNode(
p.random(40, 360),
p.random(30, 270),
null, null
));
}
};
p.draw = function() {
const colors = getThemeColors();
p.noStroke();
p.fill(...colors.bg, 38);
p.rect(0, 0, 400, 300);
time += 0.009;
// Update node positions
for (let nd of nodes) {
let nx = p.noise(nd.x * 0.007 + time * 0.35, nd.y * 0.007 + time * 0.28);
let ang = nx * p.TWO_PI * 2;
nd.vx = p.lerp(nd.vx, Math.cos(ang) * 0.55, 0.04);
nd.vy = p.lerp(nd.vy, Math.sin(ang) * 0.55, 0.04);
// Anchored nodes are pulled back toward cluster center
if (nd.anchorX !== null) {
nd.vx += (nd.anchorX - nd.x) * 0.005;
nd.vy += (nd.anchorY - nd.y) * 0.005;
}
nd.x += nd.vx;
nd.y += nd.vy;
nd.x = p.constrain(nd.x, 8, 392);
nd.y = p.constrain(nd.y, 8, 292);
}
// Draw connections between nearby nodes
p.noFill();
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
let dx = nodes[i].x - nodes[j].x;
let dy = nodes[i].y - nodes[j].y;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist < CONNECTION_DIST) {
let strength = 1 - dist / CONNECTION_DIST;
let alpha = strength * strength * 145;
p.stroke(...colors.accent1, alpha);
p.strokeWeight(strength * 2.2);
p.line(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y);
}
}
}
// Draw nodes — glow brighter when well-connected
for (let nd of nodes) {
let connectionCount = 0;
for (let other of nodes) {
if (other === nd) continue;
let dx = nd.x - other.x;
let dy = nd.y - other.y;
if (Math.sqrt(dx * dx + dy * dy) < CONNECTION_DIST) connectionCount++;
}
let connected = Math.min(connectionCount / 5, 1);
let pulse = 0.82 + 0.18 * Math.sin(time * 2.1 + nd.ph);
let cr = p.lerp(colors.accent3[0], colors.accent2[0], connected);
let cg = p.lerp(colors.accent3[1], colors.accent2[1], connected);
let cb = p.lerp(colors.accent3[2], colors.accent2[2], connected);
let alpha = (0.45 + 0.55 * connected) * 210 * pulse;
p.noStroke();
p.fill(cr, cg, cb, alpha);
p.circle(nd.x, nd.y, nd.size * 2 * (1 + connected * 0.45));
}
// Caption
p.noStroke();
p.fill(...colors.accent3, 78);
p.textAlign(p.CENTER);
p.textSize(9);
p.text('proximity \u2192 connection', 200, 294);
};
};