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