Source of: Bioluminescent Constellation Swarm, by Gemini 3.8 Flash
187 lines, no comments ยท 2,702 tokens, written in 20 seconds for $0.010
1 const PARTICLE_COUNT = 450;
2 let particles = [];
3 let attractors = [];
4 let ripples = [];
5
6 function setup() {
7 createCanvas(windowWidth, windowHeight);
8 colorMode(HSB, 360, 100, 100, 100);
9 for (let i = 0; i < PARTICLE_COUNT; i++) {
10 particles.push(new Particle());
11 }
12 for (let i = 0; i < 4; i++) {
13 attractors.push(new Attractor(i));
14 }
15 }
16
17 function draw() {
18 blendMode(BLEND);
19 background(230, 40, 6, 18);
20
21 for (let a of attractors) {
22 a.update();
23 }
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 blendMode(ADD);
34 for (let p of particles) {
35 p.applyForces(attractors, ripples);
36 p.update();
37 p.display();
38 }
39 }
40
41 function mousePressed() {
42 ripples.push(new Ripple(mouseX, mouseY));
43 }
44
45 function mouseDragged() {
46 if (frameCount % 4 === 0) {
47 ripples.push(new Ripple(mouseX, mouseY, 40));
48 }
49 }
50
51 function windowResized() {
52 resizeCanvas(windowWidth, windowHeight);
53 }
54
55 class Particle {
56 constructor() {
57 this.pos = createVector(random(width), random(height));
58 this.vel = p5.Vector.random2D().mult(random(0.5, 2));
59 this.acc = createVector(0, 0);
60 this.maxSpeed = random(2.5, 5);
61 this.baseHue = random(160, 290);
62 this.hue = this.baseHue;
63 this.size = random(1.5, 3.5);
64 this.history = [];
65 this.maxHistory = floor(random(6, 14));
66 }
67
68 applyForces(attractorList, rippleList) {
69 let force = createVector(0, 0);
70
71 for (let a of attractorList) {
72 let dir = p5.Vector.sub(a.pos, this.pos);
73 let d = constrain(dir.mag(), 20, 600);
74 dir.normalize();
75 let strength = (a.strength * 60) / (d * d);
76 dir.mult(strength);
77 force.add(dir);
78 }
79
80 if (mouseIsPressed || (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height)) {
81 let mPos = createVector(mouseX, mouseY);
82 let mDir = p5.Vector.sub(mPos, this.pos);
83 let mDist = mDir.mag();
84 if (mDist < 250 && mDist > 5) {
85 mDir.normalize();
86 let mStrength = map(mDist, 0, 250, 0.4, 0);
87 if (mouseIsPressed) mStrength *= -1.5;
88 force.add(mDir.mult(mStrength));
89 }
90 }
91
92 for (let r of rippleList) {
93 let rDir = p5.Vector.sub(this.pos, r.pos);
94 let rDist = rDir.mag();
95 let ringDist = abs(rDist - r.radius);
96 if (ringDist < 25) {
97 rDir.normalize();
98 let push = map(ringDist, 0, 25, 1.2, 0) * (r.life / r.maxLife);
99 force.add(rDir.mult(push));
100 this.hue = (this.hue + 40) % 360;
101 }
102 }
103
104 let n = noise(this.pos.x * 0.003, this.pos.y * 0.003, frameCount * 0.005);
105 let angle = n * TWO_PI * 2;
106 let flow = p5.Vector.fromAngle(angle).mult(0.08);
107 force.add(flow);
108
109 this.acc.add(force);
110 }
111
112 update() {
113 this.vel.add(this.acc);
114 this.vel.limit(this.maxSpeed);
115 this.pos.add(this.vel);
116 this.acc.mult(0);
117
118 this.history.push(this.pos.copy());
119 if (this.history.length > this.maxHistory) {
120 this.history.shift();
121 }
122
123 this.hue = lerp(this.hue, this.baseHue, 0.02);
124
125 if (this.pos.x < 0) this.pos.x = width;
126 if (this.pos.x > width) this.pos.x = 0;
127 if (this.pos.y < 0) this.pos.y = height;
128 if (this.pos.y > height) this.pos.y = 0;
129 }
130
131 display() {
132 noFill();
133 for (let i = 0; i < this.history.length - 1; i++) {
134 let p1 = this.history[i];
135 let p2 = this.history[i + 1];
136 if (p1.dist(p2) > 40) continue;
137 let alpha = map(i, 0, this.history.length, 10, 85);
138 stroke(this.hue, 80, 100, alpha);
139 strokeWeight(map(i, 0, this.history.length, 0.5, this.size));
140 line(p1.x, p1.y, p2.x, p2.y);
141 }
142 noStroke();
143 fill(this.hue, 50, 100, 90);
144 circle(this.pos.x, this.pos.y, this.size * 1.5);
145 }
146 }
147
148 class Attractor {
149 constructor(offset) {
150 this.offset = offset * 1000;
151 this.pos = createVector(0, 0);
152 this.strength = random(0.8, 1.8);
153 }
154
155 update() {
156 let t = frameCount * 0.003;
157 this.pos.x = map(noise(t + this.offset), 0, 1, width * 0.1, width * 0.9);
158 this.pos.y = map(noise(t + 500 + this.offset), 0, 1, height * 0.1, height * 0.9);
159 }
160 }
161
162 class Ripple {
163 constructor(x, y, maxRadius = 120) {
164 this.pos = createVector(x, y);
165 this.radius = 0;
166 this.maxRadius = maxRadius;
167 this.maxLife = 40;
168 this.life = this.maxLife;
169 }
170
171 update() {
172 this.radius += this.maxRadius / this.maxLife;
173 this.life--;
174 }
175
176 isDead() {
177 return this.life <= 0;
178 }
179
180 display() {
181 let alpha = map(this.life, 0, this.maxLife, 0, 60);
182 noFill();
183 stroke(45, 90, 100, alpha);
184 strokeWeight(1.5);
185 circle(this.pos.x, this.pos.y, this.radius * 2);
186 }
187 }