Where ideas percolate and thoughts brew

The Narrative Trap

About This Sketch

A field of scattered events — the same data, endlessly renarrated. Every few seconds, a new "story" draws itself through the same points: a different sequence, a different set of causes and effects, equally coherent each time. The arrows suggest causality. The underlying events are random. Accompanies the post on how narrative thinking systematically distorts our understanding of the past.

Algorithm

A field of 38 randomly placed event-points sits scattered across the canvas. Every 160 frames, the sketch selects a random subset of 5–8 points and connects them in a spatially coherent sequence — picking each next point from the nearest available neighbors with a small random offset to prevent purely mechanical paths. The resulting path animates in frame by frame, drawing arrows to indicate causality. The selected points are highlighted; all others fade to near-invisible. Each new "narrative" is a different coherent story drawn from the same underlying events. This accompanies the blog post "The Narrative Trap," which argues that stories impose false causality on random events and that the same facts support infinite plausible narratives.

Pseudocode

SETUP:
  Place 38 events at random positions
  Generate first narrative path

NEW NARRATIVE:
  Increment story counter
  Shuffle events
  Pick random starting point
  For each next point: sort remaining by proximity, pick from top 4 with randomness
  Reset animation timer

DRAW (every frame):
  Get theme colors
  Clear background
  Draw title text
  Draw unselected events as faint dots
  Animate narrative path drawing (segments appear over 70 frames)
  Draw directional arrows at segment midpoints
  Highlight selected narrative points with numbered labels
  Display story counter and event count
  When timer expires: generate new narrative

Source Code

let sketch = function(p) {
    let events = [];
    let narrative = [];
    let narrativeTimer = 0;
    const NARRATIVE_DURATION = 160;
    let storyCount = 0;
    let drawProgress = 0;

    p.setup = function() {
        p.createCanvas(400, 300);
        p.textFont('Georgia');

        for (let i = 0; i < 38; i++) {
            events.push({
                x: p.random(30, 370),
                y: p.random(35, 265),
                r: p.random(2.5, 5)
            });
        }

        newNarrative();
    };

    function newNarrative() {
        storyCount++;
        let available = [...events];
        available.sort(() => Math.random() - 0.5);

        narrative = [];
        let count = Math.floor(p.random(5, 8));
        narrative.push(available.shift());

        for (let i = 1; i < count && available.length > 0; i++) {
            let last = narrative[narrative.length - 1];
            available.sort((a, b) =>
                p.dist(last.x, last.y, a.x, a.y) - p.dist(last.x, last.y, b.x, b.y)
            );
            let pick = Math.floor(p.random(0, Math.min(4, available.length)));
            narrative.push(available.splice(pick, 1)[0]);
        }

        drawProgress = 0;
        narrativeTimer = 0;
    }

    function drawArrow(colors, x1, y1, x2, y2) {
        let angle = Math.atan2(y2 - y1, x2 - x1);
        let mx = (x1 + x2) / 2;
        let my = (y1 + y2) / 2;
        p.push();
        p.translate(mx, my);
        p.rotate(angle);
        p.fill(...colors.accent1, 200);
        p.noStroke();
        p.triangle(5, 0, -3, -3, -3, 3);
        p.pop();
    }

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

        narrativeTimer++;
        if (narrativeTimer >= NARRATIVE_DURATION) newNarrative();

        drawProgress = Math.min(1, narrativeTimer / 70);

        p.fill(...colors.accent3, 180);
        p.noStroke();
        p.textAlign(p.CENTER);
        p.textSize(11);
        p.text('Same events. Different story every time.', 200, 18);

        let narrativeSet = new Set(narrative);
        for (let ev of events) {
            if (!narrativeSet.has(ev)) {
                p.fill(...colors.accent3, 55);
                p.noStroke();
                p.ellipse(ev.x, ev.y, ev.r * 2, ev.r * 2);
            }
        }

        let totalSegments = narrative.length - 1;
        let segmentsDone = Math.floor(drawProgress * totalSegments);
        let partial = (drawProgress * totalSegments) - segmentsDone;

        p.strokeWeight(1.5);
        for (let i = 0; i < segmentsDone; i++) {
            let a = narrative[i];
            let b = narrative[i + 1];
            p.stroke(...colors.accent1, 200);
            p.line(a.x, a.y, b.x, b.y);
            drawArrow(colors, a.x, a.y, b.x, b.y);
        }

        if (segmentsDone < totalSegments) {
            let a = narrative[segmentsDone];
            let b = narrative[segmentsDone + 1];
            let ex = p.lerp(a.x, b.x, partial);
            let ey = p.lerp(a.y, b.y, partial);
            p.stroke(...colors.accent1, 200 * partial);
            p.strokeWeight(1.5);
            p.line(a.x, a.y, ex, ey);
        }

        for (let i = 0; i < narrative.length; i++) {
            let ev = narrative[i];
            let visible = i / Math.max(totalSegments, 1) <= drawProgress + 0.01;
            if (visible) {
                p.fill(...colors.accent2, 230);
                p.noStroke();
                p.ellipse(ev.x, ev.y, 11, 11);
                p.fill(...colors.bg);
                p.ellipse(ev.x, ev.y, 5, 5);
                p.fill(...colors.accent2);
                p.textSize(7);
                p.textAlign(p.CENTER, p.CENTER);
                p.noStroke();
                p.text(i + 1, ev.x, ev.y - 11);
            }
        }

        p.fill(...colors.accent3, 130);
        p.textAlign(p.CENTER);
        p.textSize(8);
        p.noStroke();
        p.text('Narrative #' + storyCount + '  —  ' + events.length + ' events, infinite possible stories', 200, 291);
    };
};