Learn Creative Coding (#18) - Physics Lite: Springs, Friction, and Flocking

avatar

Learn Creative Coding (#18) - Physics Lite: Springs, Friction, and Flocking

cc-banner

Last episode we gave our sketches memory with state machines -- different modes, transitions between them, timeline sequencing. Our animations can now have structure and narrative. But inside each state, the actual motion is still pretty basic. Things drift at constant speed, follow the mouse with lerp, maybe ease into position. Nothing bounces. Nothing wobbles. Nothing feels heavy.

Real things have weight. They accelerate, decelerate, overshoot, snap back. A ball thrown upward doesn't stop instantly at the top -- it decelerates, hangs for a moment, then falls. A door on a spring doesn't just close -- it swings past center, bounces back, settles. Your brain is incredibly good at detecting motion that doesn't obey physics. We evolved tracking thrown objects and predator movements for hundreds of thousands of years, so anything that moves "wrong" triggers an immediate gut reaction of something's off.

The good news: you don't need a physics engine. You don't need to understand Newtonian mechanics. Three concepts cover 90% of what creative coding needs: springs (pull toward a target with overshoot), friction (slow things down over time), and flocking (emergent swarm behavior from simple rules). That's what this episode is about. By the end of it, your particles will feel like they have mass, your interfaces will bounce, and your swarms will self-organize into something that looks genuinely alive :-)

The core physics loop

Before we build anything specific, let's nail the foundation. All physics simulation in creative coding comes down to three variables per object:

let x = 300;       // position
let vx = 0;        // velocity (how position changes)
let ax = 0;        // acceleration (how velocity changes)

Each frame, you update them in order:

vx += ax;     // acceleration changes velocity
x += vx;      // velocity changes position
ax = 0;       // reset acceleration (forces get re-applied next frame)

That's it. That's the entire physics engine. Everything else -- gravity, springs, friction, attraction, repulsion -- is just different ways of setting ax before the update step. Gravity sets ax = 0, ay = 0.5. A spring calculates force based on distance from a target. Friction multiplies velocity by something less than 1. But the update loop is always the same three lines.

This is called Euler integration and it's what 95% of creative coding uses. It's not the most accurate method (Verlet integration is more stable for constraints like rigid bodies and cloth), but it's simple, fast, and good enough for visual work where nobody's checking the physics textbook :-)

If this reminds you of the lerp-toward-target pattern from episode 16 -- good eye. Lerp moves a fraction of the remaining distance each frame. Physics accumulates velocity. The result looks similar at first glance, but springs overshoot and oscillate where lerp never does. That's the key difference and it's why springs feel more alive.

Springs: the workhorse of creative coding physics

A spring pulls a value toward a target. Like lerp, but with momentum. It can overshoot, oscillate, and eventually settle. The math is Hooke's law: force is proportional to displacement.

class Spring {
  constructor(value, target, stiffness, damping) {
    this.value = value;
    this.target = target;
    this.velocity = 0;
    this.stiffness = stiffness;   // how hard it pulls (0.01 - 0.5)
    this.damping = damping;        // how fast oscillation dies (0.7 - 0.99)
  }

  update() {
    let force = (this.target - this.value) * this.stiffness;
    this.velocity += force;
    this.velocity *= this.damping;
    this.value += this.velocity;
    return this.value;
  }
}

That's the whole thing. Force pulls toward target. Velocity accumulates force. Damping bleeds energy. Position follows velocity. The interaction between stiffness and damping creates wildly different motion feels:

  • High stiffness, high damping (0.3, 0.85): snappy, barely overshoots. Good for buttons and UI elements.
  • Low stiffness, high damping (0.02, 0.95): slow and smooth, gentle overshoot. Dreamy, floating feel.
  • High stiffness, low damping (0.2, 0.6): bouncy, lots of oscillation. Playful, cartoonish.
  • Low stiffness, low damping (0.05, 0.7): wobbles for a long time like jelly. Organic and weird.

Let's see it work. A circle that follows your mouse with spring physics:

let springX = new Spring(300, 300, 0.08, 0.75);
let springY = new Spring(200, 200, 0.08, 0.75);

function setup() {
  createCanvas(600, 400);
}

function draw() {
  background(20);

  springX.target = mouseX;
  springY.target = mouseY;

  let x = springX.update();
  let y = springY.update();

  fill(100, 200, 255);
  noStroke();
  ellipse(x, y, 30, 30);
}

Move your mouse fast. The circle follows, but it overshoots and bounces back. It feels like it has weight. Compare this with the lerp-toward-target pattern from episode 16 -- lerp is smooth but never overshoots. Springs have energy. They go past the destination, come back, go past again (smaller each time), and settle. That's damped harmonic oscillation and it governs everything from guitar strings to car suspensions to buildings swaying in earthquakes.

A springy chain

Multiple spring-connected values create gorgeous trailing motion. Each node springs toward the previous one, forming a chain:

let nodes = [];
let numNodes = 15;

function setup() {
  createCanvas(600, 400);

  for (let i = 0; i < numNodes; i++) {
    nodes.push({
      x: width / 2,
      y: height / 2,
      vx: 0,
      vy: 0
    });
  }
}

function draw() {
  background(20, 20, 20, 30);

  // first node follows mouse directly
  nodes[0].x = mouseX;
  nodes[0].y = mouseY;

  // each subsequent node springs toward the previous one
  for (let i = 1; i < nodes.length; i++) {
    let prev = nodes[i - 1];
    let curr = nodes[i];

    let fx = (prev.x - curr.x) * 0.15;
    let fy = (prev.y - curr.y) * 0.15;

    curr.vx += fx;
    curr.vy += fy;
    curr.vx *= 0.8;
    curr.vy *= 0.8;
    curr.x += curr.vx;
    curr.y += curr.vy;
  }

  // draw the chain
  stroke(100, 200, 255, 100);
  strokeWeight(2);
  noFill();
  beginShape();
  for (let node of nodes) {
    vertex(node.x, node.y);
  }
  endShape();

  noStroke();
  for (let i = 0; i < nodes.length; i++) {
    let alpha = map(i, 0, nodes.length - 1, 255, 50);
    fill(100, 200, 255, alpha);
    ellipse(nodes[i].x, nodes[i].y, 10 - i * 0.4);
  }
}

A springy chain that follows your mouse. Each node drags the next one behind it, creating a trailing effect that fades and shrinks toward the end. Move the mouse in circles and it creates these gorgeous spiraling trails. The semi-transparent background (background(20, 20, 20, 30)) gives it that trail persistence we used in the galaxy project (episode 15).

The spring stiffness (0.15) and damping (0.8) here are tuned for a responsive, slightly bouncy chain. Try dropping the stiffness to 0.05 and the damping to 0.9 -- the chain becomes long and lazy, like an underwater tentacle. Raise the stiffness to 0.3 and reduce damping to 0.6 -- it becomes twitchy and aggressive, like an electrical arc. Same code, different personality.

Springs to grid: elastic mesh

Connect particles to grid positions with springs and you get an elastic mesh that deforms and springs back:

let gridParticles = [];
let cols = 15, rows = 10;

function setup() {
  createCanvas(600, 400);

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      let homeX = 40 + c * 38;
      let homeY = 30 + r * 38;
      gridParticles.push({
        x: homeX, y: homeY,
        homeX: homeX, homeY: homeY,
        vx: 0, vy: 0
      });
    }
  }
}

function draw() {
  background(20);

  for (let p of gridParticles) {
    // spring force toward home position
    let fx = (p.homeX - p.x) * 0.03;
    let fy = (p.homeY - p.y) * 0.03;

    // mouse repulsion
    let dx = p.x - mouseX;
    let dy = p.y - mouseY;
    let dist = Math.sqrt(dx * dx + dy * dy);

    if (dist < 100 && dist > 0) {
      let force = (100 - dist) / 100;
      fx += (dx / dist) * force * 3;
      fy += (dy / dist) * force * 3;
    }

    p.vx += fx;
    p.vy += fy;
    p.vx *= 0.92;
    p.vy *= 0.92;
    p.x += p.vx;
    p.y += p.vy;

    fill(100, 200, 255);
    noStroke();
    ellipse(p.x, p.y, 6);
  }
}

Move the mouse over the grid and the dots push away. Pull the mouse back and they spring to their home positions, wobbling and settling. The spring stiffness (0.03) is low enough that the return is gradual and bouncy. The friction (0.92) bleeds enough energy that they settle instead of oscillating forever.

This is the same basic pattern as the interactive particle art from episode 17's state machine demo -- we had particles reacting to the mouse with different forces per state. Now the forces themselves are richer. The state machine provides structure, the physics provides feel.

Friction: the simplest force

Friction is just velocity *= something_less_than_1. We've actually been using it this whole time inside our springs -- that's what the damping parameter does. But friction on its own, without springs, creates nice sliding and drifting motion:

let objects = [];

function setup() {
  createCanvas(600, 400);

  for (let i = 0; i < 30; i++) {
    objects.push({
      x: random(width),
      y: random(height),
      vx: 0,
      vy: 0,
      size: random(10, 30)
    });
  }
}

function draw() {
  background(20);

  for (let obj of objects) {
    // push away from mouse
    let dx = obj.x - mouseX;
    let dy = obj.y - mouseY;
    let dist = Math.sqrt(dx * dx + dy * dy);

    if (dist < 150 && dist > 0) {
      let force = (150 - dist) / 150;
      obj.vx += (dx / dist) * force * 2;
      obj.vy += (dy / dist) * force * 2;
    }

    // friction -- this one line does so much
    obj.vx *= 0.95;
    obj.vy *= 0.95;

    obj.x += obj.vx;
    obj.y += obj.vy;

    // bounce off walls
    if (obj.x < 0 || obj.x > width) obj.vx *= -0.8;
    if (obj.y < 0 || obj.y > height) obj.vy *= -0.8;
    obj.x = constrain(obj.x, 0, width);
    obj.y = constrain(obj.y, 0, height);

    fill(200, 100, 50);
    noStroke();
    ellipse(obj.x, obj.y, obj.size);
  }
}

Objects scatter when you mouse over them, then gradually slow down and stop. The friction value (0.95) means each frame the object keeps 95% of its velocity. That's your "surface material." Lower values = more friction = stops faster (rough concrete). Higher values = less friction = slides forever (ice rink). At 0.99 things drift lazily. At 0.85 they stop almost immediately.

The wall bounce uses *= -0.8 instead of *= -1 -- this means the object loses 20% of its energy on each bounce. That's why balls slow down when bouncing in real life. With -1 they'd bounce at full height forever, which looks wrong. That small detail makes a huge differnce in how natural the motion feels.

Combining forces

The real magic isn't any single force -- it's layering them. Gravity + spring = pendulum. Spring + friction = bouncy settlement. Repulsion between particles = self-organizing crystals. This is the same layering principle from the galaxy project (episode 15): simple rules, complex emergent behavior. The physics provides the rules; the combination provides the beauty.

Here's particles with gravity, mouse attraction, friction, AND wall bouncing:

let particles = [];

function setup() {
  createCanvas(600, 400);
  for (let i = 0; i < 80; i++) {
    particles.push({
      x: random(width), y: random(height),
      vx: random(-2, 2), vy: random(-2, 2),
      size: random(4, 12)
    });
  }
}

function draw() {
  background(20, 25);

  for (let p of particles) {
    // gravity
    p.vy += 0.15;

    // mouse attraction
    let dx = mouseX - p.x;
    let dy = mouseY - p.y;
    let dist = Math.sqrt(dx * dx + dy * dy);
    if (dist < 200 && dist > 0) {
      p.vx += (dx / dist) * 0.5;
      p.vy += (dy / dist) * 0.5;
    }

    // friction (air resistance)
    p.vx *= 0.99;
    p.vy *= 0.99;

    // velocity cap -- ALWAYS do this
    let speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
    if (speed > 10) {
      p.vx = (p.vx / speed) * 10;
      p.vy = (p.vy / speed) * 10;
    }

    p.x += p.vx;
    p.y += p.vy;

    // floor bounce
    if (p.y > height - p.size/2) {
      p.y = height - p.size/2;
      p.vy *= -0.7;
      p.vx *= 0.95;  // ground friction on bounce
    }

    // wall bounce
    if (p.x < p.size/2 || p.x > width - p.size/2) {
      p.vx *= -0.8;
      p.x = constrain(p.x, p.size/2, width - p.size/2);
    }

    fill(255, 180, 80, 200);
    noStroke();
    ellipse(p.x, p.y, p.size);
  }
}

Particles fall with gravity, bounce off the floor with energy loss, and get attracted to your mouse. Pull them upward and let go -- they arc and fall like a fountain. The semi-transparent background gives them trails. Four forces working together, each one simple, the combination surprisingly rich.

See that velocity cap in there? That's critical. Without speed limits, numerical errors accumulate and particles shoot off to infinity -- or worse, become NaN, which is even worse because NaN propagates through every calculation and your entire simulation silently collapses. I learned this the hard way at a demo day once. My physics sim looked perfect in testing, then someone dragged a particle really fast and the whole thing went to NaN. Velocity capping. Always. A simple constrain or magnitude check prevents hours of debugging.

Flocking: emergence from simplicity

This is where it gets magical. Craig Reynolds published his flocking algorithm in 1987 -- three simple rules that produce bird-like swarm behavior:

  1. Separation: steer away from nearby neighbors (don't crash)
  2. Alignment: steer toward the average heading of neighbors (fly the same direction)
  3. Cohesion: steer toward the center of nearby neighbors (stay with the group)

That's it. Three rules. No leader, no path planning, no central intelligence. Each boid (Reynolds' word, from "bird-oid") only knows about its immediate neighbors. And from this local behavior, global patterns emerge -- flocks split and rejoin, flow around obstacles, form organic streams. It's one of the most beautiful examples of emergence in computer science.

class Boid {
  constructor() {
    this.x = random(width);
    this.y = random(height);
    let angle = random(TWO_PI);
    this.vx = cos(angle) * 2;
    this.vy = sin(angle) * 2;
    this.maxSpeed = 3;
  }

  flock(boids) {
    let sepX = 0, sepY = 0, sepCount = 0;
    let aliX = 0, aliY = 0, aliCount = 0;
    let cohX = 0, cohY = 0, cohCount = 0;

    for (let other of boids) {
      if (other === this) continue;

      let dx = this.x - other.x;
      let dy = this.y - other.y;
      let dist = Math.sqrt(dx * dx + dy * dy);

      // separation -- nearby neighbors push away
      if (dist < 25 && dist > 0) {
        sepX += dx / dist;
        sepY += dy / dist;
        sepCount++;
      }

      // alignment -- steer toward average velocity
      if (dist < 50) {
        aliX += other.vx;
        aliY += other.vy;
        aliCount++;
      }

      // cohesion -- steer toward center of group
      if (dist < 80) {
        cohX += other.x;
        cohY += other.y;
        cohCount++;
      }
    }

    if (sepCount > 0) {
      this.vx += (sepX / sepCount) * 0.05;
      this.vy += (sepY / sepCount) * 0.05;
    }
    if (aliCount > 0) {
      this.vx += ((aliX / aliCount) - this.vx) * 0.02;
      this.vy += ((aliY / aliCount) - this.vy) * 0.02;
    }
    if (cohCount > 0) {
      this.vx += ((cohX / cohCount) - this.x) * 0.0005;
      this.vy += ((cohY / cohCount) - this.y) * 0.0005;
    }

    // limit speed
    let speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
    if (speed > this.maxSpeed) {
      this.vx = (this.vx / speed) * this.maxSpeed;
      this.vy = (this.vy / speed) * this.maxSpeed;
    }
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;

    // wrap around edges
    if (this.x < 0) this.x = width;
    if (this.x > width) this.x = 0;
    if (this.y < 0) this.y = height;
    if (this.y > height) this.y = 0;
  }

  draw() {
    let angle = Math.atan2(this.vy, this.vx);

    push();
    translate(this.x, this.y);
    rotate(angle);
    fill(180, 220, 255, 200);
    noStroke();
    triangle(8, 0, -4, -3, -4, 3);
    pop();
  }
}

The flock() method does all three rules in one pass through the neighbors. Each rule has a different perception radius -- separation is tight (25px, only very close neighbors matter), alignment is medium (50px), cohesion is wide (80px). This layered perception is what creates the natural-looking behavior. If all three had the same radius, the flock would look rigid and uniform.

The force strengths matter a lot: separation (0.05) is strongest, cohesion (0.0005) is weakest. Separation needs to be dominant otherwise boids pile on top of each other. Cohesion needs to be gentle otherwise the flock collapses into a tight ball. Alignment sits in between, keeping the group moving in roughly the same direction. Tweaking these three numbers changes the flock's entire personality.

Now run it:

let boids = [];

function setup() {
  createCanvas(800, 500);
  for (let i = 0; i < 150; i++) {
    boids.push(new Boid());
  }
}

function draw() {
  background(15, 15, 20);

  for (let boid of boids) {
    boid.flock(boids);
    boid.update();
    boid.draw();
  }
}

150 triangles. Nobody tells them where to go. They self-organize into flowing groups, split and rejoin, avoid collisions, and move like a murmuration of starlings. Three dead-simple rules. This is emergence -- complex global behavior from simple local rules. It never gets old. I remember the first time I saw a flocking simulation, honestly, it changed how I think about code. Hundreds of triangles moving like a school of fish. Nobody told them where to go -- they figured it out themselves.

Adding a predator

Make the mouse a predator that boids flee from:

// add this inside the flock() method, after the three rules
let mx = this.x - mouseX;
let my = this.y - mouseY;
let mouseDist = Math.sqrt(mx * mx + my * my);

if (mouseDist < 120 && mouseDist > 0) {
  // flee! stronger than the other forces
  this.vx += (mx / mouseDist) * 0.3;
  this.vy += (my / mouseDist) * 0.3;
}

Move the mouse into a group and watch them scatter. Pull it away and they regroup. It's incredibly satisfying. The flee force (0.3) is much stronger than any of the flocking forces, so it overrides normal behavior when the mouse is close -- exactly like a real predator scattering a flock. But as soon as the mouse moves away, the three flocking rules take over again and the boids reform their groups.

See how atan2(vy, vx) in the draw method points each triangle in the direction it's moving? That's the same trig from episode 13 -- converting velocity components into an angle. Without it, the boids would all face the same direction regardless of where they're going, which looks mechanical and wrong. With it, they bank and turn and it reads as alive.

When to use what

After three episodes of motion techniques (easing in ep 16, state transitions in ep 17, physics now), here's my mental model for choosing:

TechniqueFeels likeBest for
LerpSmooth, no overshootUI following, color fading, smooth chase
EasingDesigned, intentionalChoreographed animation, transitions
SpringPhysical, bouncyThings with weight and energy
FrictionSliding, deceleratingPost-interaction drift, settling
FlockingAlive, emergentSwarms, crowds, natural movement

They're not mutually exclusive. A flock of boids can use springs for their internal motion. Easing can transition between flocking parameters. Lerp can smooth a spring's target. The state machine from last episode decides WHICH forces are active, and the physics decides HOW things move within that state. Structure and feel, working together.

Bringing it together: a physics playground

Allez, let's build something that combines springs, friction, and flocking. Particles that are attracted to the mouse with spring physics, repel each other to avoid overlap, and have friction to keep things stable:

let dots = [];

function setup() {
  createCanvas(600, 400);
  for (let i = 0; i < 60; i++) {
    dots.push({
      x: random(width), y: random(height),
      vx: 0, vy: 0,
      hue: random(360)
    });
  }
}

function draw() {
  background(20);
  colorMode(HSB, 360, 100, 100);

  for (let i = 0; i < dots.length; i++) {
    let a = dots[i];

    // spring toward mouse
    a.vx += (mouseX - a.x) * 0.005;
    a.vy += (mouseY - a.y) * 0.005;

    // repel from neighbors (separation)
    for (let j = 0; j < dots.length; j++) {
      if (i === j) continue;
      let b = dots[j];
      let dx = a.x - b.x;
      let dy = a.y - b.y;
      let dist = Math.sqrt(dx * dx + dy * dy);
      if (dist < 30 && dist > 0) {
        a.vx += (dx / dist) * 0.5;
        a.vy += (dy / dist) * 0.5;
      }
    }

    // friction
    a.vx *= 0.95;
    a.vy *= 0.95;

    a.x += a.vx;
    a.y += a.vy;

    // soft edge wrapping
    if (a.x < 0) a.x = width;
    if (a.x > width) a.x = 0;
    if (a.y < 0) a.y = height;
    if (a.y > height) a.y = 0;

    fill(a.hue, 70, 90);
    noStroke();
    ellipse(a.x, a.y, 8);
  }

  colorMode(RGB, 255);
}

The dots cluster around your mouse but never pile on top of each other. The spring attraction pulls them in, the separation pushes them apart, and friction keeps things from spiraling out of control. Move the mouse slowly and they form a loose cloud that follows you. Move fast and they trail behind in a stream. It's three forces, maybe 15 lines of physics code, and it creates motion that looks like it took weeks to develop.

A practical tip about force tuning

Here's something I wish someone told me earlier: tune your forces in this order.

  1. Friction first. Set it to something reasonable (0.95 is a safe default). This determines how quickly everything settles.
  2. Primary force second. Whatever the main behavior is -- gravity, spring toward target, flocking. Get this feeling right in isolation.
  3. Secondary forces last. Repulsion, wall bouncing, mouse interaction. These are modifiers on top of the primary behavior.

If you tune everything simultaneously, you'll chase your tail forever. One force interferes with another and you can't tell which parameter to adjust. Isolate, tune, layer. Same build-in-stages approach we used for the galaxy in episode 15.

And always, always cap your velocities. I can't stress this enough. Without limits, numerical errors compound and particles fly off to infinity. A simple magnitude check after all forces are applied prevents the whole simulation from exploding:

let speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
if (speed > maxSpeed) {
  p.vx = (p.vx / speed) * maxSpeed;
  p.vy = (p.vy / speed) * maxSpeed;
}

Why physics simulations surprise you

Here's what I find deeply satisfying about physics in creative coding: the results are surprising. You set up the forces, press play, and watch things happen that you didn't explicitly program. Particles cluster in unexpected ways. Repelling particles form hexagonal grids because hexagonal packing is the most efficient arrangement in 2D -- nature figured this out with honeycombs, and your simulation discovers it independently. Pendulums syncrhonize. Springs create standing waves.

The physics knows things you don't. And that's the fun part -- setting up a system, pressing play, and being genuinely surprised by what emerges. Some of the most beautiful creative coding pieces I've seen are pure physics simulations with carefully chosen colors and initial conditions. No noise, no procedural generation -- just forces and particles and time. The complexity comes from the physics, not from the algorithm. Your job is to set up interesting initial conditions and let the math do its thing.

This is the same emergence principle we saw in the galaxy project, but physics takes it further. In the galaxy, the motion was mostly orbital with some perturbation. With springs and flocking, the motion is genuinely unpredictable -- sensitive to initial conditions, responsive to interaction, never exactly the same twice. That unpredictability is what makes it feel alive.

't Komt erop neer...

  • The core physics loop: acceleration changes velocity, velocity changes position. Three lines.
  • Springs = force toward target + velocity + damping. They overshoot and oscillate, unlike lerp.
  • Tune stiffness (pull strength) and damping (energy loss) for different feels -- snappy, bouncy, wobbly, floaty
  • Friction is just velocity *= 0.95 -- simple but essential for stability
  • Flocking uses three local rules (separation, alignment, cohesion) that produce global emergent behavior
  • Combine forces by layering: gravity + spring + friction + repulsion, each one simple, the combination complex
  • ALWAYS cap velocities to prevent NaN explosions
  • Tune forces in isolation before combining them -- friction first, primary force second, modifiers last
  • Physics simulations surprise you. Set up the rules and let the math discover things you didn't expect.

Phase 3 keeps building. We've got smooth motion (lerp and easing), structured behavior (state machines), and physical feel (springs, friction, flocking). Our sketches move naturally and have weight to them now. But so far all our visuals respond to the mouse and the keyboard -- what happens when they respond to sound? Frequencies driving particle behavior, bass making things pulse, melodies shaping the canvas. That's coming next, and it's one of my favorite topics in all of creative coding :-)

Sallukes! Thanks for reading.

X

@femdev



0
0
0.000
0 comments