Where ideas percolate and thoughts brew

The Advice Problem

About This Sketch

Particles rise from below. Most fade before reaching the top. The few survivors glow at the summit and broadcast downward rings — their experience radiating out as advice. A visualization of survivorship bias: the advice landscape is shaped entirely by the voices of those who made it, while the silent majority of paths that ended early leave no trace.

Algorithm

Many particles spawn at the bottom of the canvas and rise upward. Each particle is assigned a random fate: about 14% survive all the way to the top; the rest fade out at a random height along their ascent. Survivors reaching the top become pulsing "advisor" nodes. Each advisor periodically emits downward-expanding elliptical rings, representing their advice broadcasting outward. The rings fade as they descend. The canvas fills with a field of rising particles — most dimming and vanishing mid-journey — while a handful of glowing survivors accumulate at the top, broadcasting rings into the space below. This sketch accompanies the blog post "The Advice Problem" and visualizes survivorship bias: you only hear from the ones who made it.

Pseudocode

SETUP:
  Initialize canvas (400x300)
  Pre-populate particles at random heights with random survival fates

DRAW (every frame):
  Get current theme colors
  Soft-clear background (trail effect)

  Spawn new particle every 16 frames (if under limit):
    Assign random x position
    Assign survives=true (14% chance) or survives=false
    If non-surviving: assign fadeStartY (random height where it dims)

  For each particle:
    Move upward by speed
    If non-surviving and below fadeStartY: reduce alpha
    If alpha reaches 0: remove
    If surviving particle reaches top: promote to advisor
    Draw particle with color lerped from muted (bottom) to warm (top)

  For each advisor:
    Float gently with sine oscillation
    Every 52 frames: emit new downward ring
    Update rings: move down, expand radius, fade alpha
    Draw rings as thin ellipses
    Draw glowing dot with pulsing radius

  Trim advisors list to max 9
  Draw caption: "you hear from the ones who made it"

Source Code

let sketch = function(p) {
    // Many particles rise from the bottom.
    // Each has a random chance of fading out before reaching the top.
    // The few that reach the top become glowing "advisors" and broadcast
    // downward-expanding rings — their advice radiating out.
    // Most paths go silent. You only hear from those who made it.
    // Represents: survivorship bias in advice.

    let particles = [];
    let advisors = [];
    let time = 0;

    function makeParticle() {
        let survives = p.random() < 0.14;
        return {
            x: p.random(18, 382),
            y: 282,
            speed: p.random(0.45, 0.85),
            alpha: 175,
            survives: survives,
            fadeStartY: survives ? 0 : p.random(28, 245),
            size: p.random(1.8, 3.2),
            ph: p.random(p.TWO_PI)
        };
    }

    p.setup = function() {
        p.createCanvas(400, 300);
        p.colorMode(p.RGB);
        for (let i = 0; i < 55; i++) {
            let pt = makeParticle();
            pt.y = p.random(22, 282);
            if (!pt.survives && pt.y < pt.fadeStartY) {
                pt.alpha = p.random(10, 140);
            }
            particles.push(pt);
        }
    };

    p.draw = function() {
        const colors = getThemeColors();
        p.noStroke();
        p.fill(...colors.bg, 32);
        p.rect(0, 0, 400, 300);

        time += 0.011;

        if (p.frameCount % 16 === 0 && particles.length < 90) {
            particles.push(makeParticle());
        }

        for (let i = particles.length - 1; i >= 0; i--) {
            let pt = particles[i];
            pt.y -= pt.speed;

            if (!pt.survives && pt.y <= pt.fadeStartY) {
                pt.alpha -= 4.5;
            }

            if (pt.alpha <= 0) {
                particles.splice(i, 1);
                continue;
            }

            if (pt.survives && pt.y < 20) {
                advisors.push({ x: pt.x, y: 16, ph: pt.ph, rings: [], age: 0 });
                particles.splice(i, 1);
                continue;
            }

            let pct = p.constrain((282 - pt.y) / 260, 0, 1);
            let cr = p.lerp(colors.accent3[0], colors.accent1[0], pct);
            let cg = p.lerp(colors.accent3[1], colors.accent1[1], pct);
            let cb = p.lerp(colors.accent3[2], colors.accent1[2], pct);

            p.noStroke();
            p.fill(cr, cg, cb, pt.alpha);
            p.circle(pt.x, pt.y, pt.size * 2);
        }

        for (let sv of advisors) {
            sv.age++;
            sv.y = 16 + Math.sin(time * 1.7 + sv.ph) * 2.5;

            if (sv.age % 52 === 8) {
                sv.rings.push({ x: sv.x, y: sv.y + 10, alpha: 105, r: 5 });
            }

            for (let j = sv.rings.length - 1; j >= 0; j--) {
                let ring = sv.rings[j];
                ring.y += 1.3;
                ring.r += 2.8;
                ring.alpha -= 1.6;
                if (ring.alpha <= 0 || ring.y > 290) {
                    sv.rings.splice(j, 1);
                    continue;
                }
                p.noFill();
                p.stroke(...colors.accent2, ring.alpha);
                p.strokeWeight(0.9);
                p.ellipse(ring.x, ring.y, ring.r * 2, ring.r * 0.55);
            }

            let pulse = 0.82 + 0.18 * Math.sin(time * 2.3 + sv.ph);
            p.noStroke();
            p.fill(...colors.accent2, 42 * pulse);
            p.circle(sv.x, sv.y, 20 * pulse);
            p.fill(...colors.accent2, 215 * pulse);
            p.circle(sv.x, sv.y, 5.5 * pulse);
        }

        if (advisors.length > 9) advisors.shift();

        p.noStroke();
        p.fill(...colors.accent3, 78);
        p.textAlign(p.CENTER);
        p.textSize(9);
        p.text('you hear from the ones who made it', 200, 294);
    };
};