Source of: Silk Currents, by GPT-6 Astra
131 lines, 1 of them comments · 1,833 tokens, written in 37 seconds for $0.093
1 let ink;
2 let particles = [];
3 let clock = 0;
4 let paletteIndex = 0;
5 const palettes = [
6 [165, 195, 225, 275],
7 [325, 350, 20, 45],
8 [235, 265, 295, 320],
9 [110, 150, 175, 200]
10 ];
11
12 function setup() {
13 createCanvas(windowWidth, windowHeight);
14 pixelDensity(1);
15 colorMode(HSB, 360, 100, 100, 100);
16 textFont('monospace');
17 resetCurrent();
18 }
19
20 function resetCurrent() {
21 if (ink) ink.remove();
22 ink = createGraphics(width, height);
23 ink.pixelDensity(1);
24 ink.colorMode(HSB, 360, 100, 100, 100);
25 ink.background(230, 55, 5);
26 noiseSeed(floor(random(100000)));
27 clock = 0;
28 const count = floor(constrain(width * height / 650, 500, 1900));
29 particles = Array.from({ length: count }, () => makeParticle());
30 for (let i = 0; i < 45; i++) advance(false);
31 }
32
33 function makeParticle() {
34 return {
35 x: random(width),
36 y: random(height),
37 life: random(100, 420),
38 speed: random(0.8, 2.2),
39 tint: random(),
40 weight: random(0.45, 1.45)
41 };
42 }
43
44 function advance(interactive) {
45 ink.noStroke();
46 ink.fill(230, 55, 5, 4.2);
47 ink.rect(0, 0, width, height);
48 clock += 0.0025;
49 const scale = 0.0018;
50 const colors = palettes[paletteIndex];
51 const radius = min(width, height) * 0.3;
52 const active = interactive && mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height;
53
54 for (const p of particles) {
55 const oldX = p.x;
56 const oldY = p.y;
57 const n = noise(p.x * scale, p.y * scale, clock * 0.28);
58 const angle = n * TWO_PI * 2.7 + sin(p.y * 0.002 + clock) * 0.55;
59 let vx = cos(angle) * p.speed;
60 let vy = sin(angle) * p.speed;
61
62 if (active) {
63 const dx = mouseX - p.x;
64 const dy = mouseY - p.y;
65 const d = sqrt(dx * dx + dy * dy);
66 if (d < radius && d > 1) {
67 const influence = pow(1 - d / radius, 2) * 4.5;
68 vx += (dx * 0.25 - dy) / d * influence;
69 vy += (dy * 0.25 + dx) / d * influence;
70 }
71 }
72
73 p.x += vx;
74 p.y += vy;
75 p.life--;
76 if (p.life < 0 || p.x < -5 || p.x > width + 5 || p.y < -5 || p.y > height + 5) {
77 Object.assign(p, makeParticle());
78 continue;
79 }
80
81 const position = constrain(n * 0.75 + p.tint * 0.25, 0, 0.999) * (colors.length - 1);
82 const index = floor(position);
83 const h1 = colors[index];
84 const h2 = colors[index + 1];
85 const delta = ((h2 - h1 + 540) % 360) - 180;
86 const hue = (h1 + delta * (position - index) + 360) % 360;
87 const brightness = 65 + 35 * sin(PI * min(p.life / 55, 1) / 2);
88
89 ink.stroke(hue, 66, brightness, 44);
90 ink.strokeWeight(p.weight);
91 ink.line(oldX, oldY, p.x, p.y);
92 }
93 }
94
95 function draw() {
96 advance(true);
97 image(ink, 0, 0);
98
99 // A quiet editorial label, separate from the persistent drawing layer.
100 noStroke();
101 fill(230, 45, 5, 72);
102 rect(22, 22, 190, 54, 8);
103 fill(190, 12, 96, 92);
104 textSize(13);
105 text('S I L K C U R R E N T S', 35, 44);
106 fill(190, 15, 80, 65);
107 textSize(9);
108 text('A STUDY IN CONTINUOUS MOTION', 35, 62);
109
110 const hint = width < 480 ? 'MOVE · CLICK · R TO RESET' : 'MOVE TO BEND / CLICK FOR COLOR / R TO RESET';
111 textSize(10);
112 const labelWidth = textWidth(hint) + 26;
113 fill(230, 45, 5, 72);
114 rect(22, height - 49, labelWidth, 28, 7);
115 fill(190, 12, 87, 75);
116 text(hint, 35, height - 31);
117 }
118
119 function mousePressed() {
120 paletteIndex = (paletteIndex + 1) % palettes.length;
121 }
122
123 function keyPressed() {
124 if (key === 'r' || key === 'R') resetCurrent();
125 }
126
127 function windowResized() {
128 resizeCanvas(windowWidth, windowHeight);
129 resetCurrent();
130 }
131