Where ideas percolate and thoughts brew

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