Where ideas percolate and thoughts brew

The Kindness Trap

About This Sketch

A signal travels two routes from sender to receiver. On the direct path it arrives at full strength. On the softened path it passes through three cushioning rings that each absorb most of its brightness — what reaches the receiver is barely there. The sketch visualizes the core argument of The Kindness Trap: adjusting how you say something is legitimate, but softening what you say degrades the information itself.

Algorithm

Two parallel horizontal paths connect a signal emitter (left) to a receiver (right). The direct path carries signals unimpeded — they travel at full brightness and arrive clear. The softened path passes through three progressively larger "cushioning rings" — each absorbs a large fraction of the signal's brightness, so that what arrives at the receiver is barely visible. New signals are spawned periodically on both paths, cycling continuously. This sketch accompanies the blog post "The Kindness Trap" and visualizes how softening feedback content — not just delivery — degrades the information that actually reaches the recipient.

Pseudocode

SETUP:
  Initialize canvas (400x300)
  Define two horizontal paths: direct (top) and softened (bottom)
  Place three softening rings at intervals along the bottom path
  Spawn first pair of signals

DRAW (every frame):
  Get current theme colors
  Clear background
  Draw path lines (solid for direct, dashed for soft)
  Draw pulsing softening rings on bottom path
  Draw pulsing source emitter dots (left)
  Draw receiver dots (right, direct is bright, soft is dim)
  For each active signal:
    Advance signal along its path
    If soft signal crosses a ring: multiply brightness by 0.42
    Draw signal with brightness-scaled size and alpha
    Remove signal if it passes the receiver
  Every 90 frames: spawn a new pair of signals
  Draw path labels and caption

Source Code

let sketch = function(p) {
    let signals = [];
    const SIGNAL_SPEED = 1.4;
    const SPAWN_INTERVAL = 90;
    let frameCount = 0;

    const directY = 95;
    const softY = 205;
    const startX = 42;
    const endX = 358;

    const ringXs = [130, 200, 270];
    const ringRadius = [18, 20, 22];

    function drawLabels(colors) {
        p.noStroke();
        p.textSize(9);
        p.textAlign(p.LEFT);
        p.fill(...colors.accent2, 90);
        p.text('direct', startX + 4, directY - 16);
        p.fill(...colors.accent3, 80);
        p.text('softened', startX + 4, softY - 16);
    }

    function spawnSignal() {
        signals.push({
            x: startX, y: directY,
            path: 'direct',
            progress: 0,
            brightness: 1.0
        });
        signals.push({
            x: startX, y: softY,
            path: 'soft',
            progress: 0,
            brightness: 1.0,
            nextRing: 0
        });
    }

    p.setup = function() {
        p.createCanvas(400, 300);
        p.colorMode(p.RGB);
        spawnSignal();
    };

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

        if (frameCount % SPAWN_INTERVAL === 0) {
            spawnSignal();
        }

        p.stroke(...colors.accent2, 40);
        p.strokeWeight(1);
        p.line(startX, directY, endX, directY);

        p.stroke(...colors.accent3, 35);
        p.strokeWeight(1);
        for (let x = startX; x < endX; x += 14) {
            p.line(x, softY, Math.min(x + 8, endX), softY);
        }

        for (let i = 0; i < ringXs.length; i++) {
            let pulse = 0.88 + 0.12 * Math.sin(frameCount * 0.04 + i * 1.1);
            p.noFill();
            p.stroke(...colors.accent3, 28 * pulse);
            p.strokeWeight(0.8);
            p.ellipse(ringXs[i], softY, ringRadius[i] * 2 * pulse, ringRadius[i] * 1.5 * pulse);
            p.stroke(...colors.accent3, 14 * pulse);
            p.strokeWeight(0.5);
            p.ellipse(ringXs[i], softY, ringRadius[i] * 2.8 * pulse, ringRadius[i] * 2.2 * pulse);
        }

        let srcPulse = 0.85 + 0.15 * Math.sin(frameCount * 0.07);
        p.noStroke();
        p.fill(...colors.accent2, 35 * srcPulse);
        p.circle(startX, directY, 20 * srcPulse);
        p.fill(...colors.accent2, 210 * srcPulse);
        p.circle(startX, directY, 6 * srcPulse);

        p.fill(...colors.accent3, 30 * srcPulse);
        p.circle(startX, softY, 20 * srcPulse);
        p.fill(...colors.accent3, 180 * srcPulse);
        p.circle(startX, softY, 6 * srcPulse);

        p.fill(...colors.accent2, 25);
        p.circle(endX, directY, 18);
        p.fill(...colors.accent2, 170);
        p.circle(endX, directY, 7);

        p.fill(...colors.accent3, 18);
        p.circle(endX, softY, 18);
        p.fill(...colors.accent3, 80);
        p.circle(endX, softY, 7);

        let toRemove = [];
        for (let i = 0; i < signals.length; i++) {
            let s = signals[i];
            s.x += SIGNAL_SPEED;

            if (s.path === 'soft' && s.nextRing < ringXs.length) {
                if (s.x >= ringXs[s.nextRing]) {
                    s.brightness *= 0.42;
                    s.nextRing++;
                }
            }

            if (s.path === 'direct') {
                p.noStroke();
                p.fill(...colors.accent2, 35);
                p.circle(s.x, s.y, 16);
                p.fill(...colors.accent2, 210);
                p.circle(s.x, s.y, 7);
            } else {
                let alpha = Math.max(30, 180 * s.brightness);
                let size = Math.max(2.5, 7 * Math.sqrt(s.brightness));
                p.noStroke();
                p.fill(...colors.accent3, Math.max(10, 30 * s.brightness));
                p.circle(s.x, s.y, 16 * s.brightness + 6);
                p.fill(...colors.accent3, alpha);
                p.circle(s.x, s.y, size);
            }

            if (s.x > endX + 10) {
                toRemove.push(i);
            }
        }
        for (let i = toRemove.length - 1; i >= 0; i--) {
            signals.splice(toRemove[i], 1);
        }

        drawLabels(colors);

        p.noStroke();
        p.textAlign(p.LEFT);
        p.textSize(8.5);
        p.fill(...colors.accent2, 90);
        p.text('clear', endX + 6, directY + 4);
        p.fill(...colors.accent3, 70);
        p.text('diluted', endX + 6, softY + 4);

        p.noStroke();
        p.fill(...colors.accent3, 60);
        p.textAlign(p.CENTER);
        p.textSize(9);
        p.text('softening content dims the signal, not just the delivery', 200, 18);
    };
};