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