187 lines, no comments ยท 2,702 tokens, written in 20 seconds for $0.010


  1  const PARTICLE_COUNT = 450;
  2  let particles = [];
  3  let attractors = [];
  4  let ripples = [];
  5  
  6  function setup() {
  7    createCanvas(windowWidth, windowHeight);
  8    colorMode(HSB, 360, 100, 100, 100);
  9    for (let i = 0; i < PARTICLE_COUNT; i++) {
 10      particles.push(new Particle());
 11    }
 12    for (let i = 0; i < 4; i++) {
 13      attractors.push(new Attractor(i));
 14    }
 15  }
 16  
 17  function draw() {
 18    blendMode(BLEND);
 19    background(230, 40, 6, 18);
 20  
 21    for (let a of attractors) {
 22      a.update();
 23    }
 24  
 25    for (let i = ripples.length - 1; i >= 0; i--) {
 26      ripples[i].update();
 27      ripples[i].display();
 28      if (ripples[i].isDead()) {
 29        ripples.splice(i, 1);
 30      }
 31    }
 32  
 33    blendMode(ADD);
 34    for (let p of particles) {
 35      p.applyForces(attractors, ripples);
 36      p.update();
 37      p.display();
 38    }
 39  }
 40  
 41  function mousePressed() {
 42    ripples.push(new Ripple(mouseX, mouseY));
 43  }
 44  
 45  function mouseDragged() {
 46    if (frameCount % 4 === 0) {
 47      ripples.push(new Ripple(mouseX, mouseY, 40));
 48    }
 49  }
 50  
 51  function windowResized() {
 52    resizeCanvas(windowWidth, windowHeight);
 53  }
 54  
 55  class Particle {
 56    constructor() {
 57      this.pos = createVector(random(width), random(height));
 58      this.vel = p5.Vector.random2D().mult(random(0.5, 2));
 59      this.acc = createVector(0, 0);
 60      this.maxSpeed = random(2.5, 5);
 61      this.baseHue = random(160, 290);
 62      this.hue = this.baseHue;
 63      this.size = random(1.5, 3.5);
 64      this.history = [];
 65      this.maxHistory = floor(random(6, 14));
 66    }
 67  
 68    applyForces(attractorList, rippleList) {
 69      let force = createVector(0, 0);
 70  
 71      for (let a of attractorList) {
 72        let dir = p5.Vector.sub(a.pos, this.pos);
 73        let d = constrain(dir.mag(), 20, 600);
 74        dir.normalize();
 75        let strength = (a.strength * 60) / (d * d);
 76        dir.mult(strength);
 77        force.add(dir);
 78      }
 79  
 80      if (mouseIsPressed || (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height)) {
 81        let mPos = createVector(mouseX, mouseY);
 82        let mDir = p5.Vector.sub(mPos, this.pos);
 83        let mDist = mDir.mag();
 84        if (mDist < 250 && mDist > 5) {
 85          mDir.normalize();
 86          let mStrength = map(mDist, 0, 250, 0.4, 0);
 87          if (mouseIsPressed) mStrength *= -1.5;
 88          force.add(mDir.mult(mStrength));
 89        }
 90      }
 91  
 92      for (let r of rippleList) {
 93        let rDir = p5.Vector.sub(this.pos, r.pos);
 94        let rDist = rDir.mag();
 95        let ringDist = abs(rDist - r.radius);
 96        if (ringDist < 25) {
 97          rDir.normalize();
 98          let push = map(ringDist, 0, 25, 1.2, 0) * (r.life / r.maxLife);
 99          force.add(rDir.mult(push));
100          this.hue = (this.hue + 40) % 360;
101        }
102      }
103  
104      let n = noise(this.pos.x * 0.003, this.pos.y * 0.003, frameCount * 0.005);
105      let angle = n * TWO_PI * 2;
106      let flow = p5.Vector.fromAngle(angle).mult(0.08);
107      force.add(flow);
108  
109      this.acc.add(force);
110    }
111  
112    update() {
113      this.vel.add(this.acc);
114      this.vel.limit(this.maxSpeed);
115      this.pos.add(this.vel);
116      this.acc.mult(0);
117  
118      this.history.push(this.pos.copy());
119      if (this.history.length > this.maxHistory) {
120        this.history.shift();
121      }
122  
123      this.hue = lerp(this.hue, this.baseHue, 0.02);
124  
125      if (this.pos.x < 0) this.pos.x = width;
126      if (this.pos.x > width) this.pos.x = 0;
127      if (this.pos.y < 0) this.pos.y = height;
128      if (this.pos.y > height) this.pos.y = 0;
129    }
130  
131    display() {
132      noFill();
133      for (let i = 0; i < this.history.length - 1; i++) {
134        let p1 = this.history[i];
135        let p2 = this.history[i + 1];
136        if (p1.dist(p2) > 40) continue;
137        let alpha = map(i, 0, this.history.length, 10, 85);
138        stroke(this.hue, 80, 100, alpha);
139        strokeWeight(map(i, 0, this.history.length, 0.5, this.size));
140        line(p1.x, p1.y, p2.x, p2.y);
141      }
142      noStroke();
143      fill(this.hue, 50, 100, 90);
144      circle(this.pos.x, this.pos.y, this.size * 1.5);
145    }
146  }
147  
148  class Attractor {
149    constructor(offset) {
150      this.offset = offset * 1000;
151      this.pos = createVector(0, 0);
152      this.strength = random(0.8, 1.8);
153    }
154  
155    update() {
156      let t = frameCount * 0.003;
157      this.pos.x = map(noise(t + this.offset), 0, 1, width * 0.1, width * 0.9);
158      this.pos.y = map(noise(t + 500 + this.offset), 0, 1, height * 0.1, height * 0.9);
159    }
160  }
161  
162  class Ripple {
163    constructor(x, y, maxRadius = 120) {
164      this.pos = createVector(x, y);
165      this.radius = 0;
166      this.maxRadius = maxRadius;
167      this.maxLife = 40;
168      this.life = this.maxLife;
169    }
170  
171    update() {
172      this.radius += this.maxRadius / this.maxLife;
173      this.life--;
174    }
175  
176    isDead() {
177      return this.life <= 0;
178    }
179  
180    display() {
181      let alpha = map(this.life, 0, this.maxLife, 0, 60);
182      noFill();
183      stroke(45, 90, 100, alpha);
184      strokeWeight(1.5);
185      circle(this.pos.x, this.pos.y, this.radius * 2);
186    }
187  }