Source of: Night Currents, by GPT-6 Astra
112 lines, 2 of them comments ยท 1,313 tokens, written in 23 seconds for $0.067
1 const palettes = [
2 ['#42e8e0', '#60a5fa', '#c4b5fd', '#fef3c7'],
3 ['#fb7185', '#ffb86b', '#fde68a', '#f472b6'],
4 ['#a3e635', '#34d399', '#22d3ee', '#e0f2fe']
5 ];
6
7 let particles = [];
8 let ink = [];
9 let paletteIndex = 0;
10 let time = 0;
11
12 function setup() {
13 createCanvas(windowWidth, windowHeight);
14 pixelDensity(1);
15 strokeCap(ROUND);
16 resetScene();
17 }
18
19 function resetScene() {
20 background(5, 9, 20);
21 noiseSeed(floor(random(100000)));
22 ink = palettes[paletteIndex].map(c => color(c));
23 const count = floor(constrain(width * height / 850, 350, 1600));
24 particles = Array.from({ length: count }, () => new Stream());
25 time = 0;
26 }
27
28 function draw() {
29 blendMode(BLEND);
30 noStroke();
31 fill(5, 9, 20, 16);
32 rect(0, 0, width, height);
33
34 blendMode(ADD);
35 for (const p of particles) p.update();
36 blendMode(BLEND);
37 time += 0.002;
38 }
39
40 class Stream {
41 constructor() {
42 this.reset(true);
43 }
44
45 reset(scatter = false) {
46 this.x = random(width);
47 this.y = random(height);
48 this.age = scatter ? random(220) : 0;
49 this.life = random(220, 480);
50 this.speed = random(0.8, 2.4);
51 this.weight = random(0.5, 1.6);
52 this.tone = floor(random(ink.length));
53 }
54
55 update() {
56 const oldX = this.x;
57 const oldY = this.y;
58 const scale = 0.0018;
59 const field = noise(this.x * scale, this.y * scale, time);
60 const angle = field * TAU * 2.5;
61
62 let vx = cos(angle) * this.speed;
63 let vy = sin(angle) * this.speed;
64
65 // A soft central eddy gives the currents an orbital structure.
66 const cx = this.x - width * 0.5;
67 const cy = this.y - height * 0.5;
68 const radius = sqrt(cx * cx + cy * cy) + 1;
69 const swirl = 0.7 * exp(-radius / max(width, height));
70 vx -= cy / radius * swirl;
71 vy += cx / radius * swirl;
72
73 // The pointer acts like a small swirling obstacle.
74 if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) {
75 const dx = this.x - mouseX;
76 const dy = this.y - mouseY;
77 const d = sqrt(dx * dx + dy * dy) + 1;
78 const force = 3.5 * exp(-d * d / 14000);
79 vx += (dx - dy * 0.7) / d * force;
80 vy += (dy + dx * 0.7) / d * force;
81 }
82
83 this.x += vx;
84 this.y += vy;
85 this.age++;
86
87 if (this.x < -5 || this.x > width + 5 || this.y < -5 || this.y > height + 5 || this.age > this.life) {
88 this.reset();
89 return;
90 }
91
92 const fade = min(1, this.age / 30, (this.life - this.age) / 45);
93 const c = ink[this.tone];
94 stroke(red(c), green(c), blue(c), 65 * max(0, fade));
95 strokeWeight(this.weight);
96 line(oldX, oldY, this.x, this.y);
97 }
98 }
99
100 function mousePressed() {
101 paletteIndex = (paletteIndex + 1) % palettes.length;
102 ink = palettes[paletteIndex].map(c => color(c));
103 }
104
105 function keyPressed() {
106 if (key === 'r' || key === 'R') resetScene();
107 }
108
109 function windowResized() {
110 resizeCanvas(windowWidth, windowHeight);
111 resetScene();
112 }