178 lines, 19 of them comments · 1,666 tokens, written in 19 seconds for $0.024


  1  // Flowfield Ribbons — p5.js (no HTML needed)
  2  // Controls:
  3  // - Click: reseed flow field + colors
  4  // - Drag mouse: add particles
  5  // - 'c': clear
  6  // - 'p': pause/resume
  7  // - 's': save PNG
  8  
  9  let cols, rows;
 10  let flow = [];
 11  let scl = 18;          // flow cell size
 12  let zoff = 0;          // noise z-offset
 13  let inc = 0.08;        // noise increment
 14  let particles = [];
 15  let numParticles = 4500;
 16  let palette = [];
 17  let paused = false;
 18  
 19  function setup() {
 20    createCanvas(windowWidth, windowHeight);
 21    pixelDensity(1);
 22  
 23    initPalette();
 24    resetField();
 25  
 26    background(8);
 27    // Seed initial particles
 28    particles = [];
 29    for (let i = 0; i < numParticles; i++) {
 30      particles.push(new Particle());
 31    }
 32  }
 33  
 34  function draw() {
 35    if (paused) return;
 36  
 37    // Subtle fade for trailing effect
 38    noStroke();
 39    fill(8, 10);
 40    rect(0, 0, width, height);
 41  
 42    // Update flow field over time
 43    computeFlow();
 44  
 45    // Draw particles
 46    for (let p of particles) {
 47      p.follow(flow);
 48      p.update();
 49      p.edges();
 50      p.show();
 51    }
 52  
 53    zoff += 0.004;
 54  }
 55  
 56  function computeFlow() {
 57    cols = floor(width / scl) + 1;
 58    rows = floor(height / scl) + 1;
 59    flow = new Array(cols * rows);
 60  
 61    let yoff = 0;
 62    for (let y = 0; y < rows; y++) {
 63      let xoff = 0;
 64      for (let x = 0; x < cols; x++) {
 65        let idx = x + y * cols;
 66        let n = noise(xoff, yoff, zoff);
 67        // angle biased to create sweeping motion
 68        let angle = n * TWO_PI * 2.0;
 69        let v = p5.Vector.fromAngle(angle);
 70        v.setMag(0.35);
 71        flow[idx] = v;
 72        xoff += inc;
 73      }
 74      yoff += inc;
 75    }
 76  }
 77  
 78  function resetField() {
 79    noiseSeed(floor(random(1e9)));
 80    zoff = random(1000);
 81  }
 82  
 83  function initPalette() {
 84    // Curated palettes (mix chooses one)
 85    const palettes = [
 86      ['#f72585', '#b5179e', '#7209b7', '#3a0ca3', '#4361ee', '#4cc9f0'],
 87      ['#ffbe0b', '#fb5607', '#ff006e', '#8338ec', '#3a86ff'],
 88      ['#06d6a0', '#1b9aaa', '#ef476f', '#ffc43d', '#073b4c'],
 89      ['#d8f3dc', '#95d5b2', '#52b788', '#2d6a4f', '#1b4332'],
 90      ['#f1faee', '#a8dadc', '#457b9d', '#1d3557', '#e63946']
 91    ];
 92    palette = random(palettes);
 93  }
 94  
 95  function mousePressed() {
 96    initPalette();
 97    resetField();
 98    // Re-seed particles positions for a fresh composition
 99    for (let p of particles) {
100      p.pos.set(random(width), random(height));
101      p.prev.set(p.pos);
102      p.vel.mult(0);
103      p.acc.mult(0);
104    }
105  }
106  
107  function mouseDragged() {
108    // Add particles where you drag
109    for (let i = 0; i < 20; i++) {
110      let p = new Particle(mouseX + random(-20, 20), mouseY + random(-20, 20));
111      particles.push(p);
112    }
113    // Keep count reasonable
114    if (particles.length > 9000) particles.splice(0, particles.length - 9000);
115  }
116  
117  function keyPressed() {
118    if (key === 'c' || key === 'C') background(8);
119    if (key === 'p' || key === 'P') paused = !paused;
120    if (key === 's' || key === 'S') saveCanvas('flowfield-ribbons', 'png');
121  }
122  
123  function windowResized() {
124    resizeCanvas(windowWidth, windowHeight);
125    background(8);
126  }
127  
128  class Particle {
129    constructor(x = random(width), y = random(height)) {
130      this.pos = createVector(x, y);
131      this.prev = this.pos.copy();
132      this.vel = createVector(0, 0);
133      this.acc = createVector(0, 0);
134  
135      this.maxSpeed = random(1.2, 3.0);
136      this.weight = random(0.5, 2.2);
137      this.col = color(random(palette));
138      this.alpha = random(40, 120);
139    }
140  
141    applyForce(f) {
142      this.acc.add(f);
143    }
144  
145    follow(field) {
146      let x = floor(this.pos.x / scl);
147      let y = floor(this.pos.y / scl);
148      x = constrain(x, 0, cols - 1);
149      y = constrain(y, 0, rows - 1);
150      let idx = x + y * cols;
151      let force = field[idx];
152      if (force) this.applyForce(force);
153    }
154  
155    update() {
156      this.vel.add(this.acc);
157      this.vel.limit(this.maxSpeed);
158      this.pos.add(this.vel);
159      this.acc.mult(0);
160    }
161  
162    show() {
163      stroke(this.col.levels[0], this.col.levels[1], this.col.levels[2], this.alpha);
164      strokeWeight(this.weight);
165      line(this.prev.x, this.prev.y, this.pos.x, this.pos.y);
166      this.prev.set(this.pos);
167    }
168  
169    edges() {
170      let wrapped = false;
171      if (this.pos.x > width) { this.pos.x = 0; wrapped = true; }
172      if (this.pos.x < 0) { this.pos.x = width; wrapped = true; }
173      if (this.pos.y > height) { this.pos.y = 0; wrapped = true; }
174      if (this.pos.y < 0) { this.pos.y = height; wrapped = true; }
175      if (wrapped) this.prev.set(this.pos);
176    }
177  }
178