Source of: Interactive Particle Flow Field, by Qwen3.7 Max
153 lines, 13 of them comments ยท 1,734 tokens, written in 20 seconds for $0.0077
1 let particles = [];
2 const NUM_PARTICLES = 800;
3 let noiseScale = 0.005;
4 let time = 0;
5 let hueOffset = 0;
6
7 function setup() {
8 createCanvas(windowWidth, windowHeight);
9 colorMode(HSB, 360, 100, 100, 100);
10 background(0, 0, 0);
11
12 for (let i = 0; i < NUM_PARTICLES; i++) {
13 particles.push({
14 x: random(width),
15 y: random(height),
16 prevX: 0,
17 prevY: 0,
18 speed: random(1, 3),
19 life: random(100, 300),
20 maxLife: 300,
21 hue: random(360)
22 });
23 particles[i].prevX = particles[i].x;
24 particles[i].prevY = particles[i].y;
25 }
26 }
27
28 function draw() {
29 // Semi-transparent background for trail effect
30 noStroke();
31 fill(0, 0, 0, 3);
32 rect(0, 0, width, height);
33
34 time += 0.002;
35 hueOffset += 0.2;
36
37 // Calculate mouse influence
38 let mouseInfluence = 0;
39 if (mouseIsPressed) {
40 mouseInfluence = 0.01;
41 }
42
43 for (let p of particles) {
44 p.prevX = p.x;
45 p.prevY = p.y;
46
47 // Calculate noise-based angle
48 let noiseVal = noise(
49 p.x * noiseScale,
50 p.y * noiseScale,
51 time
52 );
53 let angle = noiseVal * TWO_PI * 4;
54
55 // Add mouse attraction when mouse is pressed
56 if (mouseIsPressed) {
57 let dx = mouseX - p.x;
58 let dy = mouseY - p.y;
59 let dist = sqrt(dx * dx + dy * dy);
60 if (dist < 200) {
61 let attractAngle = atan2(dy, dx);
62 let strength = map(dist, 0, 200, 0.8, 0);
63 angle = lerp(angle, attractAngle, strength);
64 p.speed = lerp(p.speed, 4, 0.1);
65 }
66 }
67
68 // Move particle
69 p.x += cos(angle) * p.speed;
70 p.y += sin(angle) * p.speed;
71
72 // Update life
73 p.life--;
74
75 // Draw particle trail
76 let alpha = map(p.life, 0, p.maxLife, 0, 80);
77 let hue = (p.hue + hueOffset + noiseVal * 120) % 360;
78 let saturation = 80;
79 let brightness = 90 + sin(time * 5 + p.hue) * 10;
80
81 stroke(hue, saturation, brightness, alpha);
82 strokeWeight(1.5);
83 line(p.prevX, p.prevY, p.x, p.y);
84
85 // Reset particle if out of bounds or life expired
86 if (p.x < -10 || p.x > width + 10 ||
87 p.y < -10 || p.y > height + 10 ||
88 p.life <= 0) {
89 resetParticle(p);
90 }
91
92 // Gradually restore speed
93 p.speed = lerp(p.speed, random(1, 3), 0.01);
94 }
95
96 // Draw subtle glow at mouse position
97 if (mouseIsPressed) {
98 noStroke();
99 for (let r = 60; r > 0; r -= 10) {
100 fill((hueOffset + 180) % 360, 60, 100, map(r, 0, 60, 15, 2));
101 ellipse(mouseX, mouseY, r, r);
102 }
103 }
104 }
105
106 function resetParticle(p) {
107 // Spawn from edges or random position
108 if (random() < 0.5) {
109 let side = floor(random(4));
110 switch(side) {
111 case 0: p.x = random(width); p.y = 0; break;
112 case 1: p.x = width; p.y = random(height); break;
113 case 2: p.x = random(width); p.y = height; break;
114 case 3: p.x = 0; p.y = random(height); break;
115 }
116 } else {
117 p.x = random(width);
118 p.y = random(height);
119 }
120 p.prevX = p.x;
121 p.prevY = p.y;
122 p.life = p.maxLife;
123 p.hue = random(360);
124 p.speed = random(1, 3);
125 }
126
127 function mousePressed() {
128 // Burst of particles from mouse
129 for (let i = 0; i < 20; i++) {
130 let idx = floor(random(particles.length));
131 particles[idx].x = mouseX + random(-20, 20);
132 particles[idx].y = mouseY + random(-20, 20);
133 particles[idx].prevX = particles[idx].x;
134 particles[idx].prevY = particles[idx].y;
135 particles[idx].life = particles[idx].maxLife;
136 particles[idx].speed = random(2, 5);
137 }
138 }
139
140 function keyPressed() {
141 if (key === ' ') {
142 // Clear canvas
143 background(0, 0, 0);
144 }
145 if (key === 's' || key === 'S') {
146 saveCanvas('flow_field', 'png');
147 }
148 }
149
150 function windowResized() {
151 resizeCanvas(windowWidth, windowHeight);
152 background(0, 0, 0);
153 }