152 lines, 15 of them comments · 1,454 tokens, written in 18 seconds for $0.021


  1  // Interactive Flow Field Particles
  2  // p5.js (no HTML needed)
  3  
  4  let particles = [];
  5  let numParticles = 4500;
  6  let fieldScale = 0.006;
  7  let timeScale = 0.0008;
  8  let curlStrength = 1.6;
  9  let palettes;
 10  let palette;
 11  let bg;
 12  
 13  function setup() {
 14    createCanvas(windowWidth, windowHeight);
 15    pixelDensity(1);
 16  
 17    palettes = [
 18      ['#0B1320', '#5BC0EB', '#FDE74C', '#9BC53D', '#E55934'],
 19      ['#07020D', '#5DB7DE', '#F1E9DB', '#A39B8B', '#7161EF'],
 20      ['#0B0A07', '#F72585', '#B5179E', '#7209B7', '#3A0CA3', '#4361EE', '#4CC9F0'],
 21      ['#081C15', '#1B4332', '#2D6A4F', '#52B788', '#95D5B2', '#D8F3DC'],
 22      ['#111827', '#F43F5E', '#FB7185', '#FBBF24', '#34D399', '#60A5FA']
 23    ];
 24  
 25    resetSketch(true);
 26  }
 27  
 28  function resetSketch(full = false) {
 29    palette = random(palettes);
 30    bg = color(palette[0]);
 31  
 32    background(bg);
 33  
 34    particles.length = 0;
 35    for (let i = 0; i < numParticles; i++) {
 36      particles.push(new FlowParticle());
 37    }
 38  
 39    if (full) {
 40      // Add a soft vignette overlay by drawing a translucent rect once.
 41      noStroke();
 42      fill(0, 20);
 43      rect(0, 0, width, height);
 44    }
 45  }
 46  
 47  function draw() {
 48    // Slight fade for trails
 49    noStroke();
 50    fill(red(bg), green(bg), blue(bg), 12);
 51    rect(0, 0, width, height);
 52  
 53    // Update and draw particles
 54    for (let i = 0; i < particles.length; i++) {
 55      particles[i].step();
 56    }
 57  
 58    // Small UI hint
 59    if (frameCount < 240) {
 60      push();
 61      noStroke();
 62      fill(255, 140);
 63      textSize(12);
 64      textAlign(LEFT, BOTTOM);
 65      text('Move mouse to stir • Click to reset', 12, height - 12);
 66      pop();
 67    }
 68  }
 69  
 70  function windowResized() {
 71    resizeCanvas(windowWidth, windowHeight);
 72    background(bg);
 73    // Re-seed particles into new bounds
 74    for (let p of particles) p.respawn();
 75  }
 76  
 77  function mousePressed() {
 78    resetSketch(true);
 79  }
 80  
 81  class FlowParticle {
 82    constructor() {
 83      this.respawn();
 84      this.hueColor = color(random(palette.slice(1)));
 85      this.w = random(0.6, 1.4);
 86      this.maxSpeed = random(1.2, 2.6);
 87      this.life = int(random(120, 520));
 88    }
 89  
 90    respawn() {
 91      this.pos = createVector(random(width), random(height));
 92      this.prev = this.pos.copy();
 93      this.vel = createVector(0, 0);
 94      this.life = int(random(120, 520));
 95    }
 96  
 97    step() {
 98      this.prev.set(this.pos);
 99  
100      // Flow field direction from noise
101      const t = millis() * timeScale;
102      const nx = this.pos.x * fieldScale;
103      const ny = this.pos.y * fieldScale;
104  
105      // Mouse influence (a gentle vortex around the mouse)
106      const mdx = this.pos.x - mouseX;
107      const mdy = this.pos.y - mouseY;
108      const mDist = sqrt(mdx * mdx + mdy * mdy) + 0.0001;
109      const mouseFactor = constrain(180 / mDist, 0, 2.0);
110  
111      // Base angle from noise
112      const n = noise(nx, ny, t);
113      let ang = n * TAU * 2.0;
114  
115      // Add a subtle curl component by sampling nearby noise
116      const eps = 0.002;
117      const n1 = noise(nx + eps, ny, t);
118      const n2 = noise(nx, ny + eps, t);
119      const curl = (n1 - n2) * 12.0;
120      ang += curl * curlStrength;
121  
122      // Mouse swirl
123      const swirl = atan2(mdy, mdx) + HALF_PI; // tangent
124      ang = lerp(ang, swirl, mouseFactor * 0.35);
125  
126      const acc = p5.Vector.fromAngle(ang);
127      acc.mult(0.55);
128  
129      this.vel.add(acc);
130      this.vel.limit(this.maxSpeed);
131      this.pos.add(this.vel);
132  
133      // Draw trail segment
134      stroke(red(this.hueColor), green(this.hueColor), blue(this.hueColor), 90);
135      strokeWeight(this.w);
136      line(this.prev.x, this.prev.y, this.pos.x, this.pos.y);
137  
138      // Wrap or respawn
139      this.life--;
140      if (
141        this.life <= 0 ||
142        this.pos.x < -10 || this.pos.x > width + 10 ||
143        this.pos.y < -10 || this.pos.y > height + 10
144      ) {
145        this.respawn();
146        this.hueColor = color(random(palette.slice(1)));
147        this.w = random(0.6, 1.4);
148        this.maxSpeed = random(1.2, 2.6);
149      }
150    }
151  }
152