Source of: Flow Field Ribbons, by GPT-5.2
158 lines, 7 of them comments ยท 1,484 tokens, written in 18 seconds for $0.021
1 let particles = [];
2 let nParticles = 2200;
3 let zOff = 0;
4 let seed;
5
6 function setup() {
7 createCanvas(windowWidth, windowHeight);
8 pixelDensity(1);
9 resetSketch();
10 }
11
12 function resetSketch() {
13 seed = floor(random(1e9));
14 randomSeed(seed);
15 noiseSeed(seed);
16
17 background(10);
18 particles = [];
19
20 for (let i = 0; i < nParticles; i++) {
21 particles.push(new Particle(random(width), random(height)));
22 }
23 }
24
25 function draw() {
26 // subtle fade for trails
27 noStroke();
28 fill(10, 12);
29 rect(0, 0, width, height);
30
31 zOff += 0.002;
32
33 // flow intensity responds to mouse
34 const mx = (mouseX / max(1, width));
35 const my = (mouseY / max(1, height));
36 const flowScale = lerp(0.0015, 0.008, constrain(mx, 0, 1));
37 const forceMag = lerp(0.15, 0.9, constrain(my, 0, 1));
38
39 // draw particles
40 blendMode(ADD);
41 for (let p of particles) {
42 const a = flowAngle(p.pos.x, p.pos.y, flowScale);
43 const v = p5.Vector.fromAngle(a).mult(forceMag);
44 p.applyForce(v);
45 p.update();
46 p.show(a);
47 p.wrap();
48 }
49 blendMode(BLEND);
50
51 // occasional new particles for variation
52 if (frameCount % 30 === 0 && particles.length < nParticles + 400) {
53 for (let i = 0; i < 30; i++) {
54 particles.push(new Particle(random(width), random(height)));
55 }
56 }
57 }
58
59 function flowAngle(x, y, s) {
60 // multi-octave noise for richer curls
61 const nx = x * s;
62 const ny = y * s;
63 const n1 = noise(nx, ny, zOff);
64 const n2 = noise(nx * 2.3 + 100, ny * 2.3 - 40, zOff * 1.2);
65 const n3 = noise(nx * 5.1 - 20, ny * 5.1 + 80, zOff * 1.8);
66
67 const n = (n1 * 0.55 + n2 * 0.30 + n3 * 0.15);
68 return n * TWO_PI * 3.0;
69 }
70
71 class Particle {
72 constructor(x, y) {
73 this.pos = createVector(x, y);
74 this.prev = this.pos.copy();
75 this.vel = p5.Vector.random2D().mult(0.2);
76 this.acc = createVector(0, 0);
77
78 this.hue = random(0, 360);
79 this.w = random(0.6, 1.6);
80 this.maxSpeed = random(1.2, 3.2);
81 this.life = floor(random(240, 900));
82 }
83
84 applyForce(f) {
85 this.acc.add(f);
86 }
87
88 update() {
89 this.prev.set(this.pos);
90
91 this.vel.add(this.acc);
92 this.vel.limit(this.maxSpeed);
93 this.pos.add(this.vel);
94
95 this.acc.mult(0);
96
97 this.life--;
98 if (this.life <= 0) this.respawn();
99 }
100
101 respawn() {
102 this.pos.set(random(width), random(height));
103 this.prev.set(this.pos);
104 this.vel = p5.Vector.random2D().mult(0.2);
105 this.life = floor(random(240, 900));
106 this.hue = (this.hue + random(30, 90)) % 360;
107 }
108
109 show(a) {
110 // color tied to direction + personal hue
111 colorMode(HSB, 360, 100, 100, 100);
112 const h = (this.hue + degrees(a) * 0.15) % 360;
113 const sat = 70;
114 const bri = 95;
115 const alpha = 18;
116
117 stroke(h, sat, bri, alpha);
118 strokeWeight(this.w);
119 line(this.prev.x, this.prev.y, this.pos.x, this.pos.y);
120 }
121
122 wrap() {
123 let wrapped = false;
124
125 if (this.pos.x < 0) { this.pos.x = width; wrapped = true; }
126 else if (this.pos.x > width) { this.pos.x = 0; wrapped = true; }
127
128 if (this.pos.y < 0) { this.pos.y = height; wrapped = true; }
129 else if (this.pos.y > height) { this.pos.y = 0; wrapped = true; }
130
131 if (wrapped) this.prev.set(this.pos);
132 }
133 }
134
135 function mousePressed() {
136 resetSketch();
137 }
138
139 function keyPressed() {
140 if (key === 's' || key === 'S') {
141 saveCanvas(`flowfield_${seed}_${nf(frameCount, 6)}`, 'png');
142 }
143 if (key === 'c' || key === 'C') {
144 background(10);
145 }
146 }
147
148 function windowResized() {
149 resizeCanvas(windowWidth, windowHeight);
150 background(10);
151 // keep particles but move any out of bounds
152 for (let p of particles) {
153 p.pos.x = constrain(p.pos.x, 0, width);
154 p.pos.y = constrain(p.pos.y, 0, height);
155 p.prev.set(p.pos);
156 }
157 }
158