Where ideas percolate and thoughts brew

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