Envy Is Data
About This Sketch
Two responses to the same signal. Suppressors scatter when they get close, losing energy and coherence. Converters absorb the signal and redirect it — each toward their own distinct goal. The signal doesn't change. Only the response does.
Algorithm
Twenty-two particles navigate toward a pulsing central beacon (the envied signal).
Eleven are "suppressors" — they approach but scatter chaotically when near,
losing energy with each repulsion. Eleven are "converters" — they approach,
absorb the signal on contact, and redirect toward their own unique goal point,
gaining energy as they advance. The same signal produces two entirely different
trajectories depending on the response to it.
Pseudocode
SETUP:
Place beacon at center
Create 22 particles at random positions
Half are suppressors, half converters
Converters each have a unique goal point (right region)
DRAW each frame:
Pulse beacon with animated glow
For each particle:
If suppressor:
If within 65px of beacon: repel + add jitter, lose energy
Else if within 120px: weakly attract
If converter:
If not yet activated and within 45px: mark activated
If not activated: move toward beacon
If activated: steer toward personal goal, gain energy
Apply damping, boundary bounce
Draw with size/opacity scaled by energy
Draw legend
Source Code
let beacon;
let particles = [];
let frameCount = 0;
let sketch = function(p) {
p.setup = function() {
p.createCanvas(400, 300);
p.colorMode(p.RGB);
beacon = { x: 200, y: 150 };
for (let i = 0; i < 22; i++) {
let isConverter = i >= 11;
particles.push({
x: p.random(30, p.width - 30),
y: p.random(30, p.height - 30),
vx: p.random(-0.8, 0.8),
vy: p.random(-0.8, 0.8),
size: p.random(5, 10),
isConverter: isConverter,
goalX: isConverter ? p.random(290, 370) : null,
goalY: isConverter ? p.random(40, 260) : null,
activated: false,
energy: 0.6 + p.random(0.4),
age: p.random(100)
});
}
};
p.draw = function() {
const colors = getThemeColors();
frameCount++;
p.background(...colors.bg, 40);
// Beacon
p.noStroke();
for (let r = 6; r > 0; r--) {
let alpha = r * 18 + Math.sin(frameCount * 0.05) * 10;
p.fill(...colors.accent1, alpha);
p.ellipse(beacon.x, beacon.y, r * 16, r * 16);
}
p.fill(...colors.accent2);
p.ellipse(beacon.x, beacon.y, 16, 16);
p.noStroke();
p.fill(...colors.accent3);
p.textSize(8);
p.textAlign(p.CENTER);
p.text("signal", beacon.x, beacon.y - 16);
for (let part of particles) {
part.age++;
let dx = beacon.x - part.x;
let dy = beacon.y - part.y;
let dist = Math.sqrt(dx * dx + dy * dy) || 1;
if (!part.isConverter) {
if (dist < 65) {
part.vx -= (dx / dist) * 0.35;
part.vy -= (dy / dist) * 0.35;
part.vx += p.random(-0.25, 0.25);
part.vy += p.random(-0.25, 0.25);
part.energy = Math.max(0.25, part.energy - 0.002);
} else if (dist < 120) {
part.vx += (dx / dist) * 0.06;
part.vy += (dy / dist) * 0.06;
}
} else {
if (!part.activated && dist < 45) {
part.activated = true;
}
if (!part.activated) {
part.vx += (dx / dist) * 0.09;
part.vy += (dy / dist) * 0.09;
} else {
let gdx = part.goalX - part.x;
let gdy = part.goalY - part.y;
let gdist = Math.sqrt(gdx * gdx + gdy * gdy) || 1;
if (gdist > 6) {
part.vx += (gdx / gdist) * 0.15;
part.vy += (gdy / gdist) * 0.15;
} else {
part.vx += p.random(-0.1, 0.1);
part.vy += p.random(-0.1, 0.1);
}
part.energy = Math.min(1.0, part.energy + 0.002);
}
}
part.vx *= 0.94;
part.vy *= 0.94;
part.x += part.vx;
part.y += part.vy;
if (part.x < 8) { part.vx += 0.4; }
if (part.x > p.width - 8) { part.vx -= 0.4; }
if (part.y < 8) { part.vy += 0.4; }
if (part.y > p.height - 8) { part.vy -= 0.4; }
p.noStroke();
if (part.isConverter) {
if (part.activated && part.goalX) {
p.stroke(...colors.accent2, 25);
p.strokeWeight(0.5);
p.line(part.x, part.y, part.goalX, part.goalY);
p.noStroke();
p.fill(...colors.accent2, 70);
p.ellipse(part.goalX, part.goalY, 5, 5);
}
p.noStroke();
let alpha = part.activated ? 210 : 140;
p.fill(...colors.accent2, alpha * part.energy);
p.ellipse(part.x, part.y, part.size * (0.7 + part.energy * 0.4), part.size * (0.7 + part.energy * 0.4));
} else {
p.noStroke();
p.fill(...colors.accent1, 190 * part.energy);
p.ellipse(part.x, part.y, part.size * part.energy, part.size * part.energy);
}
}
p.noStroke();
p.fill(...colors.accent1, 190);
p.ellipse(18, p.height - 22, 7, 7);
p.fill(...colors.accent3);
p.textSize(8);
p.textAlign(p.LEFT);
p.text("suppress", 28, p.height - 18);
p.fill(...colors.accent2, 210);
p.ellipse(100, p.height - 22, 7, 7);
p.fill(...colors.accent3);
p.text("convert", 110, p.height - 18);
};
};