Source of: Bioluminescent Flow Swarm, by Gemini 3.7 Flash
146 lines, no comments ยท 2,013 tokens, written in 13 seconds for $0.0077
1 let particles = [];
2 const NUM_PARTICLES = 1200;
3 let noiseScale = 0.005;
4 let noiseZ = 0;
5 let ripples = [];
6 let baseHue = 180;
7
8 function setup() {
9 createCanvas(windowWidth, windowHeight);
10 colorMode(HSB, 360, 100, 100, 1);
11 background(0);
12 for (let i = 0; i < NUM_PARTICLES; i++) {
13 particles.push(new Particle());
14 }
15 }
16
17 function draw() {
18 blendMode(BLEND);
19 background(0, 0, 0, 0.08);
20
21 blendMode(ADD);
22 noiseZ += 0.002;
23 baseHue = (baseHue + 0.1) % 360;
24
25 for (let i = ripples.length - 1; i >= 0; i--) {
26 ripples[i].update();
27 ripples[i].display();
28 if (ripples[i].isDead()) {
29 ripples.splice(i, 1);
30 }
31 }
32
33 for (let p of particles) {
34 p.update();
35 p.edges();
36 p.display();
37 }
38 }
39
40 class Particle {
41 constructor() {
42 this.init();
43 }
44
45 init() {
46 this.pos = createVector(random(width), random(height));
47 this.prevPos = this.pos.copy();
48 this.vel = createVector(0, 0);
49 this.acc = createVector(0, 0);
50 this.maxSpeed = random(2, 4.5);
51 this.hueOffset = random(-40, 40);
52 this.life = random(100, 300);
53 this.maxLife = this.life;
54 this.weight = random(1.2, 2.5);
55 }
56
57 update() {
58 let angle = noise(this.pos.x * noiseScale, this.pos.y * noiseScale, noiseZ) * TWO_PI * 4;
59 let flowForce = p5.Vector.fromAngle(angle).mult(0.2);
60 this.acc.add(flowForce);
61
62 if (mouseIsPressed) {
63 let mouse = createVector(mouseX, mouseY);
64 let dir = p5.Vector.sub(this.pos, mouse);
65 let d = dir.mag();
66 if (d < 200 && d > 0) {
67 dir.normalize();
68 let force = (200 - d) / 200;
69 this.acc.add(dir.mult(force * 1.5));
70 }
71 }
72
73 for (let r of ripples) {
74 let d = dist(this.pos.x, this.pos.y, r.x, r.y);
75 let diff = abs(d - r.radius);
76 if (diff < 30) {
77 let push = p5.Vector.sub(this.pos, createVector(r.x, r.y)).normalize();
78 let strength = map(diff, 0, 30, 2, 0) * (r.life / r.maxLife);
79 this.acc.add(push.mult(strength));
80 }
81 }
82
83 this.vel.add(this.acc);
84 this.vel.limit(this.maxSpeed);
85 this.prevPos = this.pos.copy();
86 this.pos.add(this.vel);
87 this.acc.mult(0);
88 this.life--;
89
90 if (this.life <= 0) {
91 this.init();
92 }
93 }
94
95 edges() {
96 if (this.pos.x < 0 || this.pos.x > width || this.pos.y < 0 || this.pos.y > height) {
97 this.init();
98 }
99 }
100
101 display() {
102 let currentHue = (baseHue + this.hueOffset + 360) % 360;
103 let alpha = map(this.life, 0, this.maxLife, 0, 0.8);
104 stroke(currentHue, 80, 95, alpha);
105 strokeWeight(this.weight);
106 line(this.prevPos.x, this.prevPos.y, this.pos.x, this.pos.y);
107 }
108 }
109
110 class Ripple {
111 constructor(x, y) {
112 this.x = x;
113 this.y = y;
114 this.radius = 10;
115 this.speed = 6;
116 this.life = 60;
117 this.maxLife = 60;
118 this.hue = (baseHue + 60) % 360;
119 }
120
121 update() {
122 this.radius += this.speed;
123 this.life--;
124 }
125
126 display() {
127 noFill();
128 let alpha = map(this.life, 0, this.maxLife, 0, 0.4);
129 stroke(this.hue, 70, 100, alpha);
130 strokeWeight(2);
131 circle(this.x, this.y, this.radius * 2);
132 }
133
134 isDead() {
135 return this.life <= 0;
136 }
137 }
138
139 function mousePressed() {
140 ripples.push(new Ripple(mouseX, mouseY));
141 }
142
143 function windowResized() {
144 resizeCanvas(windowWidth, windowHeight);
145 background(0);
146 }