Where ideas percolate and thoughts brew

The Introspection Loop

About This Sketch

Particles move through a noise-driven flow field, occasionally pulled into tightening introspective spirals before re-emerging. A visual metaphor for how self-examination can trap attention in circular patterns, while the field moves on without it.

Algorithm

Particles move through a Perlin noise flow field, tracing smooth organic paths. Periodically, individual particles get pulled into introspective loops — they begin circling inward in a tightening spiral, disconnected from the flow. Eventually they exhaust the loop and re-emerge into the field.

Pseudocode

SETUP:
  Create 75 particles at random positions

DRAW:
  Overlay semi-transparent background for trail effect
  Advance time variable

  Every 50 frames:
    For each free particle, 10% chance to begin introspecting
      Record current position as loop center
      Set loop radius and lifetime

  For each particle:
    IF introspecting:
      Tighten spiral (decay radius by 0.3% per frame)
      Advance loop angle
      Move to spiral position
      Draw in accent2 color (darker, more intense)
      If lifetime expired or radius too small: release back to flow

    ELSE (flowing):
      Sample Perlin noise at particle position and time
      Move in noise-determined direction
      Wrap at canvas edges
      Draw in accent1 color (warm, lighter)

  Draw legend labels

Source Code

let sketch = function(p) {
    let particles = [];
    const NUM = 75;
    let t = 0;

    function makeParticle() {
        return {
            x: p.random(p.width),
            y: p.random(p.height),
            looping: false,
            loopCx: 0,
            loopCy: 0,
            loopAngle: 0,
            loopR: p.random(14, 24),
            loopLife: 0,
            size: p.random(2, 3.5)
        };
    }

    p.setup = function() {
        p.createCanvas(400, 300);
        p.colorMode(p.RGB);
        for (let i = 0; i < NUM; i++) {
            particles.push(makeParticle());
        }
    };

    p.draw = function() {
        const colors = getThemeColors();
        p.background(colors.bg[0], colors.bg[1], colors.bg[2], 28);

        t += 0.004;

        if (p.frameCount % 50 === 0) {
            for (let pt of particles) {
                if (!pt.looping && p.random() < 0.1) {
                    pt.looping = true;
                    pt.loopCx = pt.x;
                    pt.loopCy = pt.y;
                    pt.loopAngle = p.random(p.TWO_PI);
                    pt.loopR = p.random(14, 24);
                    pt.loopLife = p.int(p.random(160, 320));
                }
            }
        }

        for (let pt of particles) {
            if (pt.looping) {
                pt.loopR *= 0.997;
                pt.loopAngle += 0.065;
                pt.x = pt.loopCx + p.cos(pt.loopAngle) * pt.loopR;
                pt.y = pt.loopCy + p.sin(pt.loopAngle) * pt.loopR;
                pt.loopLife--;

                if (pt.loopLife <= 0 || pt.loopR < 1.5) {
                    pt.looping = false;
                    pt.x = pt.loopCx;
                    pt.y = pt.loopCy;
                    pt.loopR = p.random(14, 24);
                }

                p.noStroke();
                p.fill(colors.accent2[0], colors.accent2[1], colors.accent2[2], 210);
                p.circle(pt.x, pt.y, pt.size * 1.9);
            } else {
                let angle = p.noise(pt.x * 0.0038, pt.y * 0.0038, t) * p.TWO_PI * 2.4;
                pt.x += p.cos(angle) * 1.7;
                pt.y += p.sin(angle) * 1.7;

                if (pt.x < -6) pt.x = p.width + 6;
                if (pt.x > p.width + 6) pt.x = -6;
                if (pt.y < -6) pt.y = p.height + 6;
                if (pt.y > p.height + 6) pt.y = -6;

                p.noStroke();
                p.fill(colors.accent1[0], colors.accent1[1], colors.accent1[2], 165);
                p.circle(pt.x, pt.y, pt.size);
            }
        }

        p.textSize(8);
        p.noStroke();
        p.fill(colors.accent1[0], colors.accent1[1], colors.accent1[2], 160);
        p.textAlign(p.LEFT);
        p.text("flowing", 8, p.height - 8);
        p.fill(colors.accent2[0], colors.accent2[1], colors.accent2[2], 160);
        p.text("introspecting", 62, p.height - 8);
    };
};