The Accountability Trap
About This Sketch
Two streams of particles travel toward a goal on the right. The top stream announces its journey — bursts of acknowledgment flash, and each announcer slows, fades, and often stalls before arriving. The bottom stream moves quietly and arrives. A visualization of the substitution effect: social recognition of intent provides a cheaper version of the reward that completing the goal would have given, reducing the drive to finish.
Algorithm
Two streams of particles travel left to right toward a glowing goal zone on the right edge.
The top stream (announced goals): particles flash with a ring burst when they cross a
threshold, triggering acknowledgment bursts from nearby particles. After receiving
social acknowledgment, each announcer's velocity decelerates sharply — they stall
mid-canvas, fading toward muted grey. Few reach the goal.
The bottom stream (private goals): particles move with steady, unremarkable velocity.
No fanfare, no acknowledgment. They reach the goal zone consistently.
The contrast is the argument: announcing a goal provides a cheap identity reward that
partially substitutes for the harder reward of completion, reducing the drive to finish.
This sketch accompanies the blog post "The Accountability Trap."
Pseudocode
SETUP:
Initialize 14 announcers (top region) at random x positions 18-200
Initialize 14 quiet movers (bottom region) at random x positions 18-200
Goal zone at x=370, both y-centers
DRAW (every frame):
Get theme colors
Fade background (trail persistence)
Draw pulsing goal zone at right edge for each stream
Draw divider line at y=150
FOR each announcer:
IF not yet announced AND x > random(65, 120):
Mark as announced
Spawn 5 acknowledgment burst particles
Set stall timer (90-200 frames of reduced speed)
IF announced:
Decelerate (stall timer counts down)
Draw expanding ring pulse
Color shifts from warm accent2 toward muted accent3 proportional to stall
Move particle
Remove if past goal or off left edge
FOR each ack burst:
Move outward
Fade alpha by 3.2/frame
Remove when transparent
FOR each quiet mover:
Move at steady speed
Draw in warm accent2
Remove if past goal or off left edge
Draw caption text
Source Code
let sketch = function(p) {
// Two groups of particles travel left to right toward a glowing goal zone.
// ANNOUNCERS (top): flash when they begin, draw acknowledgment bursts from neighbors.
// After being acknowledged, they slow and often stall before reaching the goal.
// QUIET MOVERS (bottom): no announcement, just steady motion. They reach the goal.
// Represents: announcing goals substitutes the reward of achievement, reducing drive.
let announcers = [];
let quietMovers = [];
let time = 0;
const GOAL_X = 370;
const ANN_Y_CENTER = 90;
const QUIET_Y_CENTER = 210;
function makeAnnouncer() {
return {
x: p.random(18, 50),
y: ANN_Y_CENTER + p.random(-28, 28),
vx: p.random(0.5, 0.9),
vy: p.random(-0.1, 0.1),
announced: false,
announceAge: 0,
acknowledgeCount: 0,
stall: 0,
alpha: 200,
ph: p.random(p.TWO_PI),
size: p.random(2.4, 3.6)
};
}
function makeQuietMover() {
return {
x: p.random(18, 50),
y: QUIET_Y_CENTER + p.random(-28, 28),
vx: p.random(0.55, 1.0),
vy: p.random(-0.08, 0.08),
alpha: 200,
ph: p.random(p.TWO_PI),
size: p.random(2.4, 3.6)
};
}
let ackBursts = [];
p.setup = function() {
p.createCanvas(400, 300);
p.colorMode(p.RGB);
for (let i = 0; i < 14; i++) {
let a = makeAnnouncer();
a.x = p.random(18, 200);
announcers.push(a);
}
for (let i = 0; i < 14; i++) {
let q = makeQuietMover();
q.x = p.random(18, 200);
quietMovers.push(q);
}
};
p.draw = function() {
const colors = getThemeColors();
p.noStroke();
p.fill(...colors.bg, 35);
p.rect(0, 0, 400, 300);
time += 0.014;
if (p.frameCount % 90 === 0 && announcers.length < 18) {
announcers.push(makeAnnouncer());
}
if (p.frameCount % 85 === 0 && quietMovers.length < 18) {
quietMovers.push(makeQuietMover());
}
for (let row of [ANN_Y_CENTER, QUIET_Y_CENTER]) {
let pulse = 0.75 + 0.25 * Math.sin(time * 2.1);
p.noStroke();
p.fill(...colors.accent1, 18 * pulse);
p.ellipse(GOAL_X + 10, row, 68 * pulse, 58 * pulse);
p.fill(...colors.accent1, 40 * pulse);
p.ellipse(GOAL_X + 10, row, 16 * pulse, 14 * pulse);
}
p.stroke(...colors.accent3, 40);
p.strokeWeight(1);
p.line(14, 150, 386, 150);
p.noStroke();
p.fill(...colors.accent3, 85);
p.textSize(8.5);
p.textAlign(p.LEFT);
p.text('announced goal', 16, ANN_Y_CENTER - 38);
p.text('private goal', 16, QUIET_Y_CENTER - 38);
for (let i = announcers.length - 1; i >= 0; i--) {
let a = announcers[i];
if (!a.announced && a.x > p.random(65, 120)) {
a.announced = true;
a.announceAge = 0;
for (let k = 0; k < 5; k++) {
let angle = p.random(p.TWO_PI);
let speed = p.random(0.6, 1.3);
ackBursts.push({
x: a.x + p.random(-18, 18),
y: ANN_Y_CENTER + p.random(-22, 22),
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed * 0.4,
alpha: 160
});
}
a.acknowledgeCount = p.floor(p.random(3, 7));
a.stall = p.floor(p.random(90, 200));
}
if (a.announced) {
a.announceAge++;
if (a.stall > 0) {
a.stall--;
a.vx = p.lerp(a.vx, 0.08, 0.03);
} else {
a.vx = p.lerp(a.vx, p.random(0.2, 0.45), 0.015);
}
} else {
a.vx = p.lerp(a.vx, p.random(0.5, 0.8), 0.02);
}
a.x += a.vx;
a.y += a.vy;
a.y = p.constrain(a.y, ANN_Y_CENTER - 34, ANN_Y_CENTER + 34);
if (a.announced && a.announceAge < 60) {
let ringAlpha = p.map(a.announceAge, 0, 60, 120, 0);
let ringR = p.map(a.announceAge, 0, 60, 4, 26);
p.noFill();
p.stroke(...colors.accent2, ringAlpha);
p.strokeWeight(1.2);
p.circle(a.x, a.y, ringR * 2);
}
let stalledFrac = a.announced ? p.constrain(1 - (a.vx - 0.05) / 0.6, 0, 1) : 0;
let cr = p.lerp(colors.accent2[0], colors.accent3[0], stalledFrac);
let cg = p.lerp(colors.accent2[1], colors.accent3[1], stalledFrac);
let cb = p.lerp(colors.accent2[2], colors.accent3[2], stalledFrac);
p.noStroke();
p.fill(cr, cg, cb, a.alpha * (1 - stalledFrac * 0.5));
p.circle(a.x, a.y, a.size * 2);
if (a.x > GOAL_X + 30 || a.x < 8) {
announcers.splice(i, 1);
}
}
for (let i = ackBursts.length - 1; i >= 0; i--) {
let b = ackBursts[i];
b.x += b.vx;
b.y += b.vy;
b.alpha -= 3.2;
if (b.alpha <= 0) { ackBursts.splice(i, 1); continue; }
p.noStroke();
p.fill(...colors.accent1, b.alpha * 0.6);
p.circle(b.x, b.y, 3.5);
}
for (let i = quietMovers.length - 1; i >= 0; i--) {
let q = quietMovers[i];
q.x += q.vx;
q.y += q.vy;
q.y = p.constrain(q.y, QUIET_Y_CENTER - 34, QUIET_Y_CENTER + 34);
p.noStroke();
p.fill(...colors.accent2, q.alpha);
p.circle(q.x, q.y, q.size * 2);
if (q.x > GOAL_X + 30 || q.x < 8) {
quietMovers.splice(i, 1);
}
}
p.noStroke();
p.fill(...colors.accent3, 72);
p.textAlign(p.CENTER);
p.textSize(9);
p.text('announcing replaces the drive to achieve', 200, 294);
};
};