Source of: Flowfield Ribbons (Interactive Generative Art), by GPT-5.2
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