104 lines, 9 of them comments ยท 1,959 tokens, written in 11 seconds for $0.0011


  1  // Flow Field Particles
  2  // Particles trace a Perlin noise vector field, leaving colored trails.
  3  // Move the mouse to push particles away.
  4  
  5  let particles = [];
  6  const NUM_PARTICLES = 1400;
  7  const NOISE_SCALE = 0.0035;
  8  let t = 0;
  9  let hueShift = 0;
 10  
 11  function setup() {
 12    createCanvas(windowWidth, windowHeight);
 13    colorMode(HSB, 360, 100, 100, 1);
 14    background(0, 0, 4);
 15    for (let i = 0; i < NUM_PARTICLES; i++) {
 16      particles.push(new Particle(random(width), random(height)));
 17    }
 18  }
 19  
 20  function draw() {
 21    // Fade the previous frame to create soft trails
 22    noStroke();
 23    fill(0, 0, 4, 0.035);
 24    rect(0, 0, width, height);
 25  
 26    hueShift = (hueShift + 0.25) % 360;
 27  
 28    for (const p of particles) {
 29      p.update();
 30      p.display();
 31    }
 32  
 33    t += 0.004;
 34  }
 35  
 36  class Particle {
 37    constructor(x, y) {
 38      this.pos = createVector(x, y);
 39      this.prev = this.pos.copy();
 40      this.vel = p5.Vector.random2D().mult(random(1, 3));
 41      this.hue = random(360);
 42      this.wrapped = false;
 43    }
 44  
 45    update() {
 46      this.prev.set(this.pos);
 47  
 48      // Sample the flow field
 49      const angle = noise(
 50        this.pos.x * NOISE_SCALE,
 51        this.pos.y * NOISE_SCALE,
 52        t
 53      ) * TWO_PI * 3;
 54  
 55      const force = p5.Vector.fromAngle(angle).mult(0.55);
 56      this.vel.add(force);
 57      this.vel.limit(2.8);
 58      this.pos.add(this.vel);
 59  
 60      // Mouse repulsion
 61      const d = dist(this.pos.x, this.pos.y, mouseX, mouseY);
 62      if (d < 160 && d > 0.01) {
 63        const push = p5.Vector.sub(this.pos, createVector(mouseX, mouseY));
 64        push.setMag(map(d, 0, 160, 4, 0));
 65        this.pos.add(push);
 66      }
 67  
 68      // Wrap edges (and flag so we don't draw across the screen)
 69      this.wrapped = false;
 70      if (this.pos.x < 0)  { this.pos.x += width;  this.wrapped = true; }
 71      if (this.pos.x > width)  { this.pos.x -= width;  this.wrapped = true; }
 72      if (this.pos.y < 0)  { this.pos.y += height; this.wrapped = true; }
 73      if (this.pos.y > height) { this.pos.y -= height; this.wrapped = true; }
 74  
 75      this.hue = (this.hue + 0.5) % 360;
 76    }
 77  
 78    display() {
 79      if (this.wrapped) return;
 80  
 81      // Color shifts along the direction of travel for a silky palette
 82      const h = (this.hue + hueShift + this.vel.heading() * 20) % 360;
 83      stroke((h + 360) % 360, 70, 95, 0.55);
 84      strokeWeight(1.2);
 85      line(this.prev.x, this.prev.y, this.pos.x, this.pos.y);
 86    }
 87  }
 88  
 89  function windowResized() {
 90    resizeCanvas(windowWidth, windowHeight);
 91    background(0, 0, 4);
 92  }
 93  
 94  function keyPressed() {
 95    // Press 'r' to reseed the field and clear the canvas
 96    if (key === 'r' || key === 'R') {
 97      noiseSeed(floor(random(100000)));
 98      background(0, 0, 4);
 99      particles = [];
100      for (let i = 0; i < NUM_PARTICLES; i++) {
101        particles.push(new Particle(random(width), random(height)));
102      }
103    }
104  }