@kernel.chat/kbot 2.10.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,803 @@
1
+ // K:BOT Creative Intelligence Tools
2
+ // Generate art, shaders, music patterns, SVGs, and evolve designs.
3
+ // All outputs are self-contained files — no external dependencies at generation time.
4
+ import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
5
+ import { dirname, basename, extname, join } from 'node:path';
6
+ import { registerTool } from './index.js';
7
+ // ─── Helpers ────────────────────────────────────────────────────────
8
+ /** Seed a simple deterministic PRNG from a string */
9
+ function hashSeed(s) {
10
+ let h = 0;
11
+ for (let i = 0; i < s.length; i++) {
12
+ h = ((h << 5) - h + s.charCodeAt(i)) | 0;
13
+ }
14
+ return Math.abs(h);
15
+ }
16
+ /** Pick a seeded-random float in [min, max) */
17
+ function seededRandom(seed, index) {
18
+ const x = Math.sin(seed + index * 9301 + 49297) * 233280;
19
+ return x - Math.floor(x);
20
+ }
21
+ /** Pick a random item from an array using seed */
22
+ function pick(arr, seed, index) {
23
+ return arr[Math.floor(seededRandom(seed, index) * arr.length)];
24
+ }
25
+ /** Generate a CSS hex color from seed */
26
+ function seedColor(seed, idx) {
27
+ const h = Math.floor(seededRandom(seed, idx) * 360);
28
+ const s = 50 + Math.floor(seededRandom(seed, idx + 1) * 40);
29
+ const l = 30 + Math.floor(seededRandom(seed, idx + 2) * 40);
30
+ return `hsl(${h}, ${s}%, ${l}%)`;
31
+ }
32
+ function ensureDir(path) {
33
+ mkdirSync(dirname(path), { recursive: true });
34
+ }
35
+ // ─── p5.js Art Generator ────────────────────────────────────────────
36
+ function generateP5Sketch(description, style) {
37
+ const seed = hashSeed(description);
38
+ const palette = Array.from({ length: 5 }, (_, i) => seedColor(seed, i * 3));
39
+ const bgColor = seedColor(seed, 100);
40
+ const sketchBodies = {
41
+ abstract: `
42
+ // Abstract expressionist composition
43
+ const shapes = ${3 + Math.floor(seededRandom(seed, 10) * 12)};
44
+ for (let i = 0; i < shapes; i++) {
45
+ const x = random(width);
46
+ const y = random(height);
47
+ const sz = random(20, 200);
48
+ fill(palette[i % palette.length]);
49
+ noStroke();
50
+ if (random() > 0.5) {
51
+ ellipse(x, y, sz, sz * random(0.5, 1.5));
52
+ } else {
53
+ rect(x - sz/2, y - sz/2, sz, sz * random(0.5, 1.5));
54
+ }
55
+ }
56
+ // Gestural lines
57
+ for (let i = 0; i < ${2 + Math.floor(seededRandom(seed, 11) * 6)}; i++) {
58
+ stroke(palette[i % palette.length]);
59
+ strokeWeight(random(1, 8));
60
+ noFill();
61
+ beginShape();
62
+ for (let j = 0; j < 8; j++) {
63
+ curveVertex(random(width), random(height));
64
+ }
65
+ endShape();
66
+ }`,
67
+ geometric: `
68
+ // Geometric pattern
69
+ const cols = ${3 + Math.floor(seededRandom(seed, 20) * 8)};
70
+ const rows = ${3 + Math.floor(seededRandom(seed, 21) * 8)};
71
+ const cellW = width / cols;
72
+ const cellH = height / rows;
73
+ noStroke();
74
+ for (let r = 0; r < rows; r++) {
75
+ for (let c = 0; c < cols; c++) {
76
+ const x = c * cellW;
77
+ const y = r * cellH;
78
+ fill(palette[(r + c) % palette.length]);
79
+ const shape = (r * cols + c + ${seed}) % 4;
80
+ push();
81
+ translate(x + cellW/2, y + cellH/2);
82
+ rotate(((r + c) * PI) / ${2 + Math.floor(seededRandom(seed, 22) * 4)});
83
+ if (shape === 0) rect(-cellW/2, -cellH/2, cellW, cellH);
84
+ else if (shape === 1) ellipse(0, 0, cellW, cellH);
85
+ else if (shape === 2) triangle(-cellW/2, cellH/2, cellW/2, cellH/2, 0, -cellH/2);
86
+ else { beginShape(); for (let i = 0; i < 6; i++) { const a = (TWO_PI / 6) * i; vertex(cos(a) * cellW/2, sin(a) * cellH/2); } endShape(CLOSE); }
87
+ pop();
88
+ }
89
+ }`,
90
+ organic: `
91
+ // Organic flow field
92
+ const scale = ${10 + Math.floor(seededRandom(seed, 30) * 20)};
93
+ const particles = ${200 + Math.floor(seededRandom(seed, 31) * 800)};
94
+ noStroke();
95
+ for (let i = 0; i < particles; i++) {
96
+ let x = random(width);
97
+ let y = random(height);
98
+ fill(palette[i % palette.length] + '40');
99
+ for (let step = 0; step < 50; step++) {
100
+ const angle = noise(x / scale, y / scale, ${seededRandom(seed, 32).toFixed(2)}) * TWO_PI * 2;
101
+ x += cos(angle) * 2;
102
+ y += sin(angle) * 2;
103
+ ellipse(x, y, 3, 3);
104
+ if (x < 0 || x > width || y < 0 || y > height) break;
105
+ }
106
+ }`,
107
+ fractal: `
108
+ // Recursive fractal tree
109
+ const maxDepth = ${4 + Math.floor(seededRandom(seed, 40) * 5)};
110
+ const branchAngle = ${15 + Math.floor(seededRandom(seed, 41) * 30)};
111
+ const lengthRatio = ${(0.6 + seededRandom(seed, 42) * 0.2).toFixed(2)};
112
+
113
+ const branch = (x, y, len, angle, depth) => {
114
+ if (depth >= maxDepth || len < 2) return;
115
+ const x2 = x + cos(radians(angle)) * len;
116
+ const y2 = y - sin(radians(angle)) * len;
117
+ stroke(palette[depth % palette.length]);
118
+ strokeWeight(map(depth, 0, maxDepth, 6, 1));
119
+ line(x, y, x2, y2);
120
+ branch(x2, y2, len * lengthRatio, angle + branchAngle, depth + 1);
121
+ branch(x2, y2, len * lengthRatio, angle - branchAngle, depth + 1);
122
+ if (random() > 0.5) branch(x2, y2, len * lengthRatio * 0.8, angle + branchAngle * 0.5, depth + 1);
123
+ };
124
+ branch(width/2, height, height * 0.3, 90, 0);`,
125
+ noise: `
126
+ // Perlin noise landscape
127
+ const resolution = ${2 + Math.floor(seededRandom(seed, 50) * 4)};
128
+ const zOff = ${seededRandom(seed, 51).toFixed(3)};
129
+ loadPixels();
130
+ for (let x = 0; x < width; x += resolution) {
131
+ for (let y = 0; y < height; y += resolution) {
132
+ const n = noise(x * 0.005, y * 0.005, zOff);
133
+ const ci = floor(n * palette.length);
134
+ const c = color(palette[ci % palette.length]);
135
+ for (let dx = 0; dx < resolution; dx++) {
136
+ for (let dy = 0; dy < resolution; dy++) {
137
+ const idx = 4 * ((y + dy) * width + (x + dx));
138
+ pixels[idx] = red(c);
139
+ pixels[idx+1] = green(c);
140
+ pixels[idx+2] = blue(c);
141
+ pixels[idx+3] = 255;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ updatePixels();
147
+ // Overlay contour lines
148
+ noFill();
149
+ stroke(255, 80);
150
+ strokeWeight(0.5);
151
+ for (let threshold = 0.1; threshold < 1; threshold += 0.1) {
152
+ beginShape();
153
+ for (let x = 0; x < width; x += 4) {
154
+ const y = height * noise(x * 0.005, threshold * 5, zOff + 1);
155
+ vertex(x, y);
156
+ }
157
+ endShape();
158
+ }`,
159
+ };
160
+ const body = sketchBodies[style] || sketchBodies.abstract;
161
+ return `<!DOCTYPE html>
162
+ <html lang="en">
163
+ <head>
164
+ <meta charset="UTF-8">
165
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
166
+ <title>K:BOT Art — ${description}</title>
167
+ <script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"><\/script>
168
+ <style>
169
+ * { margin: 0; padding: 0; box-sizing: border-box; }
170
+ body { display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #111; }
171
+ canvas { border-radius: 4px; box-shadow: 0 4px 24px rgba(0,0,0,0.5); }
172
+ </style>
173
+ </head>
174
+ <body>
175
+ <script>
176
+ // Generated by K:BOT creative tools
177
+ // Description: ${description}
178
+ // Style: ${style}
179
+ // Seed: ${seed}
180
+
181
+ const palette = ${JSON.stringify(palette)};
182
+
183
+ function setup() {
184
+ createCanvas(800, 800);
185
+ background('${bgColor}');
186
+ noiseSeed(${seed});
187
+ randomSeed(${seed});
188
+ draw_art();
189
+ noLoop();
190
+ }
191
+
192
+ function draw_art() {
193
+ ${body}
194
+ }
195
+
196
+ function mousePressed() {
197
+ // Click to regenerate with new random seed
198
+ noiseSeed(millis());
199
+ randomSeed(millis());
200
+ background('${bgColor}');
201
+ draw_art();
202
+ }
203
+ <\/script>
204
+ </body>
205
+ </html>`;
206
+ }
207
+ // ─── GLSL Shader Generator ─────────────────────────────────────────
208
+ function generateGLSL(description) {
209
+ const seed = hashSeed(description);
210
+ const r1 = seededRandom(seed, 0);
211
+ const r2 = seededRandom(seed, 1);
212
+ const r3 = seededRandom(seed, 2);
213
+ const r4 = seededRandom(seed, 3);
214
+ const r5 = seededRandom(seed, 4);
215
+ const octaves = 3 + Math.floor(r1 * 5);
216
+ const speed = (0.2 + r2 * 1.5).toFixed(2);
217
+ const zoom = (1.0 + r3 * 4.0).toFixed(2);
218
+ const colorShiftA = (r4 * 6.28).toFixed(3);
219
+ const colorShiftB = (r5 * 6.28).toFixed(3);
220
+ const technique = Math.floor(r1 * 4);
221
+ let core = '';
222
+ if (technique === 0) {
223
+ // Fractal noise with domain warping
224
+ core = `
225
+ // Domain-warped fractal noise
226
+ vec2 q = vec2(fbm(uv + vec2(0.0, 0.0)),
227
+ fbm(uv + vec2(5.2, 1.3)));
228
+ vec2 r2 = vec2(fbm(uv + 4.0 * q + vec2(1.7, 9.2) + ${speed} * iTime),
229
+ fbm(uv + 4.0 * q + vec2(8.3, 2.8) + ${speed} * 0.7 * iTime));
230
+ float f = fbm(uv + 4.0 * r2);
231
+ vec3 color = mix(vec3(0.1, 0.05, 0.2), vec3(0.9, 0.4, 0.2), clamp(f * f * 2.0, 0.0, 1.0));
232
+ color = mix(color, vec3(0.0, 0.2, 0.6), clamp(length(q), 0.0, 1.0));
233
+ color = mix(color, vec3(0.9, 0.8, 0.5), clamp(length(r2.x), 0.0, 1.0));`;
234
+ }
235
+ else if (technique === 1) {
236
+ // Raymarched SDF
237
+ core = `
238
+ // Raymarched distance field
239
+ vec3 ro = vec3(0.0, 0.0, -3.0);
240
+ vec3 rd = normalize(vec3(uv, 1.5));
241
+ float t = 0.0;
242
+ vec3 color = vec3(0.02);
243
+ for (int i = 0; i < 80; i++) {
244
+ vec3 p = ro + rd * t;
245
+ float d = sceneSDF(p, iTime * ${speed});
246
+ if (d < 0.001) {
247
+ vec3 n = calcNormal(p, iTime * ${speed});
248
+ vec3 light = normalize(vec3(1.0, 1.0, -1.0));
249
+ float diff = max(dot(n, light), 0.0);
250
+ float spec = pow(max(dot(reflect(-light, n), -rd), 0.0), 32.0);
251
+ color = vec3(0.2, 0.4, 0.8) * diff + vec3(1.0) * spec * 0.5;
252
+ color += vec3(0.05, 0.02, 0.1);
253
+ float ao = 1.0 - float(i) / 80.0;
254
+ color *= ao;
255
+ break;
256
+ }
257
+ t += d;
258
+ if (t > 20.0) break;
259
+ }
260
+ color += vec3(0.02, 0.01, 0.04) * (1.0 - length(uv));`;
261
+ }
262
+ else if (technique === 2) {
263
+ // Kaleidoscopic pattern
264
+ core = `
265
+ // Kaleidoscopic symmetry
266
+ vec2 p = uv * ${zoom};
267
+ float a = atan(p.y, p.x);
268
+ float r = length(p);
269
+ float segments = ${3 + Math.floor(r2 * 10)}.0;
270
+ a = mod(a, 6.28318 / segments) - 3.14159 / segments;
271
+ p = vec2(cos(a), sin(a)) * r;
272
+ float pattern = sin(p.x * 10.0 + iTime * ${speed}) * cos(p.y * 10.0 - iTime * ${speed} * 0.7);
273
+ pattern += sin(r * 8.0 - iTime * ${speed} * 1.5) * 0.5;
274
+ pattern = sin(pattern * 3.14159 * 2.0);
275
+ vec3 color = 0.5 + 0.5 * cos(vec3(${colorShiftA}, ${colorShiftB}, 4.0) + pattern * 2.0 + iTime * 0.3);
276
+ color *= smoothstep(0.0, 0.02, abs(pattern));
277
+ color *= 1.0 - 0.3 * r;`;
278
+ }
279
+ else {
280
+ // Voronoi cells
281
+ core = `
282
+ // Animated Voronoi cells
283
+ vec2 p = uv * ${zoom};
284
+ float minDist = 1.0;
285
+ float secondDist = 1.0;
286
+ vec2 minPoint = vec2(0.0);
287
+ for (int y = -1; y <= 1; y++) {
288
+ for (int x = -1; x <= 1; x++) {
289
+ vec2 cell = floor(p) + vec2(float(x), float(y));
290
+ vec2 point = cell + hash2(cell);
291
+ point += 0.5 * sin(iTime * ${speed} + 6.28 * hash2(cell));
292
+ float d = length(p - point);
293
+ if (d < minDist) {
294
+ secondDist = minDist;
295
+ minDist = d;
296
+ minPoint = point;
297
+ } else if (d < secondDist) {
298
+ secondDist = d;
299
+ }
300
+ }
301
+ }
302
+ float edge = secondDist - minDist;
303
+ vec3 color = 0.5 + 0.5 * cos(vec3(${colorShiftA}, ${colorShiftB}, 4.0) + minPoint.x * 3.0 + minPoint.y * 2.0);
304
+ color *= smoothstep(0.0, 0.05, edge);
305
+ color += 0.1 * (1.0 - minDist);`;
306
+ }
307
+ return `// Generated by K:BOT Creative Tools
308
+ // Description: ${description}
309
+ // Shadertoy-compatible GLSL fragment shader
310
+ // Paste into https://www.shadertoy.com/new
311
+
312
+ // --- Utility functions ---
313
+
314
+ vec2 hash2(vec2 p) {
315
+ p = vec2(dot(p, vec2(127.1, 311.7)),
316
+ dot(p, vec2(269.5, 183.3)));
317
+ return fract(sin(p) * 43758.5453);
318
+ }
319
+
320
+ float hash(vec2 p) {
321
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
322
+ }
323
+
324
+ float noise(vec2 p) {
325
+ vec2 i = floor(p);
326
+ vec2 f = fract(p);
327
+ f = f * f * (3.0 - 2.0 * f);
328
+ float a = hash(i);
329
+ float b = hash(i + vec2(1.0, 0.0));
330
+ float c = hash(i + vec2(0.0, 1.0));
331
+ float d = hash(i + vec2(1.0, 1.0));
332
+ return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
333
+ }
334
+
335
+ float fbm(vec2 p) {
336
+ float v = 0.0;
337
+ float a = 0.5;
338
+ mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));
339
+ for (int i = 0; i < ${octaves}; i++) {
340
+ v += a * noise(p);
341
+ p = rot * p * 2.0;
342
+ a *= 0.5;
343
+ }
344
+ return v;
345
+ }
346
+
347
+ // SDF primitives (used by technique 1)
348
+ float sdSphere(vec3 p, float r) { return length(p) - r; }
349
+ float sdBox(vec3 p, vec3 b) { vec3 d = abs(p) - b; return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0)); }
350
+
351
+ float sceneSDF(vec3 p, float t) {
352
+ float d = sdSphere(p, 1.0 + 0.2 * sin(t * 2.0));
353
+ p.xz *= mat2(cos(t * 0.3), sin(t * 0.3), -sin(t * 0.3), cos(t * 0.3));
354
+ d = min(d, sdBox(p - vec3(0.0, sin(t), 0.0), vec3(0.5 + 0.2 * sin(t * 1.3))));
355
+ // Repetition
356
+ vec3 rp = mod(p + 2.0, 4.0) - 2.0;
357
+ d = min(d, sdSphere(rp, 0.3));
358
+ return d;
359
+ }
360
+
361
+ vec3 calcNormal(vec3 p, float t) {
362
+ vec2 e = vec2(0.001, 0.0);
363
+ return normalize(vec3(
364
+ sceneSDF(p + e.xyy, t) - sceneSDF(p - e.xyy, t),
365
+ sceneSDF(p + e.yxy, t) - sceneSDF(p - e.yxy, t),
366
+ sceneSDF(p + e.yyx, t) - sceneSDF(p - e.yyx, t)
367
+ ));
368
+ }
369
+
370
+ // --- Main ---
371
+
372
+ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
373
+ vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
374
+ ${core}
375
+
376
+ // Vignette
377
+ float vig = 1.0 - 0.4 * dot(uv, uv);
378
+ color *= vig;
379
+
380
+ // Gamma correction
381
+ color = pow(color, vec3(0.4545));
382
+
383
+ fragColor = vec4(color, 1.0);
384
+ }
385
+ `;
386
+ }
387
+ // ─── Music Pattern Generator ────────────────────────────────────────
388
+ function generateMusicPattern(description, genre, format) {
389
+ const seed = hashSeed(description + genre);
390
+ // Scale definitions
391
+ const scales = {
392
+ major: [0, 2, 4, 5, 7, 9, 11],
393
+ minor: [0, 2, 3, 5, 7, 8, 10],
394
+ pentatonic: [0, 2, 4, 7, 9],
395
+ blues: [0, 3, 5, 6, 7, 10],
396
+ dorian: [0, 2, 3, 5, 7, 9, 10],
397
+ mixolydian: [0, 2, 4, 5, 7, 9, 10],
398
+ };
399
+ // Genre → scale + tempo mapping
400
+ const genreConfig = {
401
+ ambient: { scale: 'pentatonic', tempoMin: 60, tempoMax: 80, noteNames: [':C4', ':E4', ':G4', ':A4', ':C5', ':E5'] },
402
+ electronic: { scale: 'minor', tempoMin: 120, tempoMax: 140, noteNames: [':C3', ':Eb3', ':G3', ':Bb3', ':C4', ':Eb4', ':G4'] },
403
+ jazz: { scale: 'dorian', tempoMin: 90, tempoMax: 130, noteNames: [':C3', ':D3', ':Eb3', ':G3', ':A3', ':C4', ':D4'] },
404
+ classical: { scale: 'major', tempoMin: 80, tempoMax: 120, noteNames: [':C4', ':D4', ':E4', ':F4', ':G4', ':A4', ':B4', ':C5'] },
405
+ hiphop: { scale: 'blues', tempoMin: 80, tempoMax: 100, noteNames: [':C2', ':Eb2', ':F2', ':Gb2', ':G2', ':Bb2', ':C3'] },
406
+ default: { scale: 'major', tempoMin: 100, tempoMax: 130, noteNames: [':C4', ':D4', ':E4', ':F4', ':G4', ':A4', ':B4'] },
407
+ };
408
+ const config = genreConfig[genre.toLowerCase()] || genreConfig.default;
409
+ const tempo = config.tempoMin + Math.floor(seededRandom(seed, 0) * (config.tempoMax - config.tempoMin));
410
+ const scaleNotes = scales[config.scale] || scales.major;
411
+ const noteCount = 8 + Math.floor(seededRandom(seed, 1) * 16);
412
+ const baseOctave = 60; // MIDI middle C
413
+ if (format === 'sonic-pi') {
414
+ // Generate Sonic Pi code
415
+ const notes = Array.from({ length: noteCount }, (_, i) => {
416
+ return pick(config.noteNames, seed, i + 10);
417
+ });
418
+ const durations = Array.from({ length: noteCount }, (_, i) => {
419
+ return pick([0.25, 0.5, 0.5, 1, 1, 2], seed, i + 100);
420
+ });
421
+ const synthChoices = ['prophet', 'saw', 'tb303', 'blade', 'pluck', 'piano'];
422
+ const fxChoices = ['reverb', 'echo', 'flanger'];
423
+ const synth = pick(synthChoices, seed, 200);
424
+ const fx = pick(fxChoices, seed, 201);
425
+ let code = `# Generated by K:BOT Creative Tools
426
+ # Description: ${description}
427
+ # Genre: ${genre}
428
+ # Tempo: ${tempo} BPM
429
+
430
+ use_bpm ${tempo}
431
+ use_synth :${synth}
432
+
433
+ `;
434
+ // Melody line
435
+ code += `# Melody\nlive_loop :melody do\n with_fx :${fx}, mix: 0.4 do\n`;
436
+ for (let i = 0; i < notes.length; i++) {
437
+ const amp = (0.4 + seededRandom(seed, i + 300) * 0.5).toFixed(2);
438
+ code += ` play ${notes[i]}, amp: ${amp}, release: ${durations[i]}\n`;
439
+ code += ` sleep ${durations[i]}\n`;
440
+ }
441
+ code += ` end\nend\n\n`;
442
+ // Bass line
443
+ const bassNotes = Array.from({ length: 4 }, (_, i) => {
444
+ return pick([':C2', ':F2', ':G2', ':Bb2'], seed, i + 400);
445
+ });
446
+ code += `# Bass\nlive_loop :bass do\n use_synth :fm\n`;
447
+ for (const note of bassNotes) {
448
+ code += ` play ${note}, amp: 0.6, release: 0.8\n sleep 1\n`;
449
+ }
450
+ code += `end\n\n`;
451
+ // Drum pattern
452
+ code += `# Drums\nlive_loop :drums do\n sample :bd_haus, amp: 0.8\n sleep 0.5\n`;
453
+ code += ` sample :sn_dub, amp: 0.5 if one_in(2)\n sleep 0.25\n`;
454
+ code += ` sample :drum_cymbal_closed, amp: 0.3\n sleep 0.25\n`;
455
+ code += `end\n`;
456
+ return code;
457
+ }
458
+ // JSON format — MIDI-like pattern
459
+ const pattern = {
460
+ meta: { description, genre, tempo, scale: config.scale, seed },
461
+ tracks: [],
462
+ };
463
+ // Note names for MIDI
464
+ const noteNameFromMidi = (midi) => {
465
+ const names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
466
+ return `${names[midi % 12]}${Math.floor(midi / 12) - 1}`;
467
+ };
468
+ // Melody track
469
+ const melodyNotes = [];
470
+ let currentTime = 0;
471
+ for (let i = 0; i < noteCount; i++) {
472
+ const scaleIdx = Math.floor(seededRandom(seed, i + 10) * scaleNotes.length);
473
+ const octaveShift = Math.floor(seededRandom(seed, i + 50) * 2) * 12;
474
+ const pitch = baseOctave + scaleNotes[scaleIdx] + octaveShift;
475
+ const velocity = 60 + Math.floor(seededRandom(seed, i + 70) * 60);
476
+ const duration = pick([0.25, 0.5, 0.5, 1, 1, 1.5, 2], seed, i + 90);
477
+ melodyNotes.push({ pitch, velocity, start: parseFloat(currentTime.toFixed(3)), duration, name: noteNameFromMidi(pitch) });
478
+ currentTime += duration;
479
+ }
480
+ pattern.tracks.push({ name: 'melody', instrument: 'piano', notes: melodyNotes });
481
+ // Bass track
482
+ const bassTrackNotes = [];
483
+ currentTime = 0;
484
+ for (let i = 0; i < Math.floor(noteCount / 2); i++) {
485
+ const scaleIdx = Math.floor(seededRandom(seed, i + 500) * scaleNotes.length);
486
+ const pitch = 36 + scaleNotes[scaleIdx]; // bass octave
487
+ const duration = pick([1, 1, 2, 2, 4], seed, i + 520);
488
+ bassTrackNotes.push({ pitch, velocity: 80, start: parseFloat(currentTime.toFixed(3)), duration, name: noteNameFromMidi(pitch) });
489
+ currentTime += duration;
490
+ }
491
+ pattern.tracks.push({ name: 'bass', instrument: 'bass', notes: bassTrackNotes });
492
+ // Drum track
493
+ const drumNotes = [];
494
+ const totalBeats = Math.ceil(currentTime);
495
+ for (let beat = 0; beat < totalBeats; beat++) {
496
+ // Kick on 1 and 3
497
+ if (beat % 2 === 0)
498
+ drumNotes.push({ pitch: 36, velocity: 100, start: beat, duration: 0.25, name: 'kick' });
499
+ // Snare on 2 and 4
500
+ if (beat % 2 === 1)
501
+ drumNotes.push({ pitch: 38, velocity: 90, start: beat, duration: 0.25, name: 'snare' });
502
+ // Hi-hat on every beat
503
+ drumNotes.push({ pitch: 42, velocity: 60, start: beat, duration: 0.125, name: 'hihat' });
504
+ // Extra hi-hat on off-beats sometimes
505
+ if (seededRandom(seed, beat + 600) > 0.4) {
506
+ drumNotes.push({ pitch: 42, velocity: 40, start: beat + 0.5, duration: 0.125, name: 'hihat' });
507
+ }
508
+ }
509
+ pattern.tracks.push({ name: 'drums', instrument: 'drums', notes: drumNotes });
510
+ return JSON.stringify(pattern, null, 2);
511
+ }
512
+ // ─── SVG Art Generator ──────────────────────────────────────────────
513
+ function generateSVG(description, width, height) {
514
+ const seed = hashSeed(description);
515
+ const palette = Array.from({ length: 6 }, (_, i) => seedColor(seed, i * 3));
516
+ const bgColor = seedColor(seed, 100);
517
+ const technique = Math.floor(seededRandom(seed, 0) * 5);
518
+ let elements = '';
519
+ if (technique === 0) {
520
+ // Concentric circles with rotation
521
+ const numCircles = 8 + Math.floor(seededRandom(seed, 10) * 12);
522
+ const cx = width / 2;
523
+ const cy = height / 2;
524
+ for (let i = 0; i < numCircles; i++) {
525
+ const r = ((i + 1) / numCircles) * Math.min(width, height) * 0.45;
526
+ const strokeW = 1 + seededRandom(seed, i + 20) * 4;
527
+ const dashLen = 5 + Math.floor(seededRandom(seed, i + 30) * 20);
528
+ const dashGap = 3 + Math.floor(seededRandom(seed, i + 40) * 15);
529
+ const color = palette[i % palette.length];
530
+ const rotation = Math.floor(seededRandom(seed, i + 50) * 360);
531
+ elements += ` <circle cx="${cx}" cy="${cy}" r="${r.toFixed(1)}" fill="none" stroke="${color}" stroke-width="${strokeW.toFixed(1)}" stroke-dasharray="${dashLen} ${dashGap}" transform="rotate(${rotation} ${cx} ${cy})" opacity="0.8"/>\n`;
532
+ }
533
+ }
534
+ else if (technique === 1) {
535
+ // Voronoi-like polygons
536
+ const numPoints = 10 + Math.floor(seededRandom(seed, 10) * 20);
537
+ const points = Array.from({ length: numPoints }, (_, i) => ({
538
+ x: seededRandom(seed, i * 2 + 100) * width,
539
+ y: seededRandom(seed, i * 2 + 101) * height,
540
+ }));
541
+ // Draw Delaunay-ish triangulation as lines
542
+ for (let i = 0; i < points.length; i++) {
543
+ // Connect to 2-3 nearest other points
544
+ const dists = points
545
+ .map((p, j) => ({ j, d: Math.hypot(p.x - points[i].x, p.y - points[i].y) }))
546
+ .filter((x) => x.j !== i)
547
+ .sort((a, b) => a.d - b.d);
548
+ const conns = 2 + Math.floor(seededRandom(seed, i + 200) * 2);
549
+ for (let k = 0; k < Math.min(conns, dists.length); k++) {
550
+ const target = points[dists[k].j];
551
+ elements += ` <line x1="${points[i].x.toFixed(1)}" y1="${points[i].y.toFixed(1)}" x2="${target.x.toFixed(1)}" y2="${target.y.toFixed(1)}" stroke="${palette[k % palette.length]}" stroke-width="1" opacity="0.5"/>\n`;
552
+ }
553
+ // Draw a circle at each point
554
+ const r = 3 + seededRandom(seed, i + 300) * 8;
555
+ elements += ` <circle cx="${points[i].x.toFixed(1)}" cy="${points[i].y.toFixed(1)}" r="${r.toFixed(1)}" fill="${palette[i % palette.length]}" opacity="0.7"/>\n`;
556
+ }
557
+ }
558
+ else if (technique === 2) {
559
+ const rects = [];
560
+ function subdivide(x, y, w, h, depth) {
561
+ if (depth > 4 || w < 30 || h < 30) {
562
+ rects.push({ x, y, w, h, depth });
563
+ return;
564
+ }
565
+ if (seededRandom(seed, depth * 100 + rects.length) > 0.4) {
566
+ // Split horizontally
567
+ const split = 0.3 + seededRandom(seed, depth * 100 + rects.length + 1) * 0.4;
568
+ subdivide(x, y, w * split, h, depth + 1);
569
+ subdivide(x + w * split, y, w * (1 - split), h, depth + 1);
570
+ }
571
+ else {
572
+ // Split vertically
573
+ const split = 0.3 + seededRandom(seed, depth * 100 + rects.length + 2) * 0.4;
574
+ subdivide(x, y, w, h * split, depth + 1);
575
+ subdivide(x, y + h * split, w, h * (1 - split), depth + 1);
576
+ }
577
+ }
578
+ subdivide(0, 0, width, height, 0);
579
+ for (let i = 0; i < rects.length; i++) {
580
+ const r = rects[i];
581
+ const fill = seededRandom(seed, i + 400) > 0.6 ? palette[i % palette.length] : bgColor;
582
+ elements += ` <rect x="${r.x.toFixed(1)}" y="${r.y.toFixed(1)}" width="${r.w.toFixed(1)}" height="${r.h.toFixed(1)}" fill="${fill}" stroke="#222" stroke-width="3"/>\n`;
583
+ }
584
+ }
585
+ else if (technique === 3) {
586
+ // Spirograph-like curves
587
+ const numCurves = 3 + Math.floor(seededRandom(seed, 10) * 5);
588
+ for (let c = 0; c < numCurves; c++) {
589
+ const R = 100 + seededRandom(seed, c * 10 + 11) * 150;
590
+ const r = 20 + seededRandom(seed, c * 10 + 12) * 80;
591
+ const d = 30 + seededRandom(seed, c * 10 + 13) * 100;
592
+ const steps = 500;
593
+ let pathData = '';
594
+ for (let i = 0; i <= steps; i++) {
595
+ const t = (i / steps) * Math.PI * 20;
596
+ const x = width / 2 + (R - r) * Math.cos(t) + d * Math.cos(((R - r) / r) * t);
597
+ const y = height / 2 + (R - r) * Math.sin(t) - d * Math.sin(((R - r) / r) * t);
598
+ pathData += i === 0 ? `M${x.toFixed(1)},${y.toFixed(1)}` : ` L${x.toFixed(1)},${y.toFixed(1)}`;
599
+ }
600
+ elements += ` <path d="${pathData}" fill="none" stroke="${palette[c % palette.length]}" stroke-width="0.8" opacity="0.7"/>\n`;
601
+ }
602
+ }
603
+ else {
604
+ // Layered wave lines
605
+ const numWaves = 8 + Math.floor(seededRandom(seed, 10) * 12);
606
+ for (let w = 0; w < numWaves; w++) {
607
+ const yBase = (height / (numWaves + 1)) * (w + 1);
608
+ const amp = 10 + seededRandom(seed, w + 20) * 50;
609
+ const freq = 0.01 + seededRandom(seed, w + 30) * 0.03;
610
+ const phase = seededRandom(seed, w + 40) * Math.PI * 2;
611
+ let pathData = '';
612
+ for (let x = 0; x <= width; x += 2) {
613
+ const y = yBase + Math.sin(x * freq + phase) * amp + Math.sin(x * freq * 2.3 + phase * 0.7) * amp * 0.3;
614
+ pathData += x === 0 ? `M${x},${y.toFixed(1)}` : ` L${x},${y.toFixed(1)}`;
615
+ }
616
+ elements += ` <path d="${pathData}" fill="none" stroke="${palette[w % palette.length]}" stroke-width="${(1 + seededRandom(seed, w + 50) * 3).toFixed(1)}" opacity="0.7" stroke-linecap="round"/>\n`;
617
+ }
618
+ }
619
+ return `<?xml version="1.0" encoding="UTF-8"?>
620
+ <!-- Generated by K:BOT Creative Tools -->
621
+ <!-- Description: ${description} -->
622
+ <!-- Seed: ${seed} -->
623
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
624
+ <rect width="100%" height="100%" fill="${bgColor}"/>
625
+ ${elements}</svg>
626
+ `;
627
+ }
628
+ // ─── Design Evolution ───────────────────────────────────────────────
629
+ function evolveDesign(source, mutationIndex) {
630
+ const lines = source.split('\n');
631
+ const seed = hashSeed(source + String(mutationIndex));
632
+ // Strategies for mutation
633
+ const mutated = lines.map((line, lineIdx) => {
634
+ let result = line;
635
+ // Mutate numeric values with some probability
636
+ result = result.replace(/(\d+\.?\d*)/g, (match, num) => {
637
+ const val = parseFloat(num);
638
+ if (isNaN(val) || seededRandom(seed, lineIdx * 100 + mutationIndex) > 0.3)
639
+ return match;
640
+ const factor = 0.7 + seededRandom(seed, lineIdx * 100 + mutationIndex + 1) * 0.6; // 0.7 to 1.3
641
+ const mutated = val * factor;
642
+ // Preserve integer vs float
643
+ return match.includes('.') ? mutated.toFixed(match.split('.')[1]?.length || 2) : String(Math.round(mutated));
644
+ });
645
+ // Mutate hex colors
646
+ result = result.replace(/#([0-9a-fA-F]{6})/g, (match, hex) => {
647
+ if (seededRandom(seed, lineIdx * 200 + mutationIndex) > 0.4)
648
+ return match;
649
+ const r = parseInt(hex.substring(0, 2), 16);
650
+ const g = parseInt(hex.substring(2, 4), 16);
651
+ const b = parseInt(hex.substring(4, 6), 16);
652
+ const shift = Math.floor((seededRandom(seed, lineIdx * 200 + mutationIndex + 1) - 0.5) * 60);
653
+ const clamp = (v) => Math.max(0, Math.min(255, v + shift));
654
+ return `#${clamp(r).toString(16).padStart(2, '0')}${clamp(g).toString(16).padStart(2, '0')}${clamp(b).toString(16).padStart(2, '0')}`;
655
+ });
656
+ // Mutate hsl colors
657
+ result = result.replace(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/g, (match, h, s, l) => {
658
+ if (seededRandom(seed, lineIdx * 300 + mutationIndex) > 0.4)
659
+ return match;
660
+ const hShift = Math.floor((seededRandom(seed, lineIdx * 300 + mutationIndex + 1) - 0.5) * 60);
661
+ const newH = (parseInt(h) + hShift + 360) % 360;
662
+ const newS = Math.max(0, Math.min(100, parseInt(s) + Math.floor((seededRandom(seed, lineIdx * 300 + mutationIndex + 2) - 0.5) * 20)));
663
+ const newL = Math.max(0, Math.min(100, parseInt(l) + Math.floor((seededRandom(seed, lineIdx * 300 + mutationIndex + 3) - 0.5) * 20)));
664
+ return `hsl(${newH}, ${newS}%, ${newL}%)`;
665
+ });
666
+ return result;
667
+ });
668
+ // Occasionally duplicate or remove a line (structural mutation)
669
+ const finalLines = [...mutated];
670
+ if (seededRandom(seed, 999) > 0.7 && finalLines.length > 5) {
671
+ // Duplicate a random line
672
+ const idx = Math.floor(seededRandom(seed, 998) * finalLines.length);
673
+ finalLines.splice(idx, 0, finalLines[idx]);
674
+ }
675
+ if (seededRandom(seed, 997) > 0.8 && finalLines.length > 10) {
676
+ // Remove a random non-critical line
677
+ const idx = 2 + Math.floor(seededRandom(seed, 996) * (finalLines.length - 4));
678
+ finalLines.splice(idx, 1);
679
+ }
680
+ return finalLines.join('\n');
681
+ }
682
+ // ─── Tool Registration ──────────────────────────────────────────────
683
+ export function registerCreativeTools() {
684
+ registerTool({
685
+ name: 'generate_art',
686
+ description: 'Generate a self-contained p5.js sketch as an HTML file from a text description. Creates generative art that can be opened directly in a browser.',
687
+ parameters: {
688
+ description: { type: 'string', description: 'Text description of the desired artwork (e.g., "flowing ocean waves at sunset")', required: true },
689
+ style: { type: 'string', description: 'Art style: "abstract", "geometric", "organic", "fractal", or "noise". Defaults to "abstract".' },
690
+ output_path: { type: 'string', description: 'File path to write the HTML file', required: true },
691
+ },
692
+ tier: 'free',
693
+ async execute(args) {
694
+ const description = String(args.description);
695
+ const style = String(args.style || 'abstract');
696
+ const validStyles = ['abstract', 'geometric', 'organic', 'fractal', 'noise'];
697
+ if (!validStyles.includes(style)) {
698
+ return `Error: Invalid style "${style}". Choose from: ${validStyles.join(', ')}`;
699
+ }
700
+ const outputPath = String(args.output_path);
701
+ const html = generateP5Sketch(description, style);
702
+ ensureDir(outputPath);
703
+ writeFileSync(outputPath, html);
704
+ return `Generated p5.js art (${style} style) at ${outputPath} (${html.length} bytes)\nOpen in a browser to view. Click the canvas to regenerate.`;
705
+ },
706
+ });
707
+ registerTool({
708
+ name: 'generate_shader',
709
+ description: 'Generate a Shadertoy-compatible GLSL fragment shader from a text description. Creates animated procedural graphics using noise, raymarching, voronoi, or kaleidoscopic techniques.',
710
+ parameters: {
711
+ description: { type: 'string', description: 'Text description of the desired shader effect (e.g., "molten lava flowing through cracks")', required: true },
712
+ output_path: { type: 'string', description: 'File path to write the .glsl file', required: true },
713
+ },
714
+ tier: 'free',
715
+ async execute(args) {
716
+ const description = String(args.description);
717
+ const outputPath = String(args.output_path);
718
+ const glsl = generateGLSL(description);
719
+ ensureDir(outputPath);
720
+ writeFileSync(outputPath, glsl);
721
+ return `Generated GLSL shader at ${outputPath} (${glsl.length} bytes)\nPaste into https://www.shadertoy.com/new to preview.`;
722
+ },
723
+ });
724
+ registerTool({
725
+ name: 'generate_music_pattern',
726
+ description: 'Generate a music pattern as Sonic Pi code or a MIDI-like JSON structure. Creates melodies, bass lines, and drum patterns based on genre and description.',
727
+ parameters: {
728
+ description: { type: 'string', description: 'Text description of the desired music (e.g., "upbeat jazz with walking bass")', required: true },
729
+ genre: { type: 'string', description: 'Music genre: "ambient", "electronic", "jazz", "classical", "hiphop". Defaults to "classical".' },
730
+ format: { type: 'string', description: 'Output format: "sonic-pi" or "json". Defaults to "sonic-pi".', required: true },
731
+ output_path: { type: 'string', description: 'File path to write the output file', required: true },
732
+ },
733
+ tier: 'free',
734
+ async execute(args) {
735
+ const description = String(args.description);
736
+ const genre = String(args.genre || 'classical');
737
+ const format = String(args.format || 'sonic-pi');
738
+ if (format !== 'sonic-pi' && format !== 'json') {
739
+ return `Error: Invalid format "${format}". Choose "sonic-pi" or "json".`;
740
+ }
741
+ const outputPath = String(args.output_path);
742
+ const content = generateMusicPattern(description, genre, format);
743
+ ensureDir(outputPath);
744
+ writeFileSync(outputPath, content);
745
+ const ext = format === 'json' ? 'JSON' : 'Sonic Pi';
746
+ return `Generated ${ext} music pattern at ${outputPath} (${content.length} bytes)\n${format === 'sonic-pi' ? 'Open in Sonic Pi (https://sonic-pi.net/) to play.' : 'Load the JSON into a DAW or MIDI player.'}`;
747
+ },
748
+ });
749
+ registerTool({
750
+ name: 'generate_svg',
751
+ description: 'Generate algorithmic SVG art from a text description. Creates generative patterns including concentric circles, mesh networks, Mondrian grids, spirographs, or wave patterns.',
752
+ parameters: {
753
+ description: { type: 'string', description: 'Text description of the desired SVG artwork', required: true },
754
+ width: { type: 'number', description: 'SVG width in pixels. Defaults to 800.' },
755
+ height: { type: 'number', description: 'SVG height in pixels. Defaults to 800.' },
756
+ output_path: { type: 'string', description: 'File path to write the .svg file', required: true },
757
+ },
758
+ tier: 'free',
759
+ async execute(args) {
760
+ const description = String(args.description);
761
+ const width = typeof args.width === 'number' ? args.width : 800;
762
+ const height = typeof args.height === 'number' ? args.height : 800;
763
+ if (width < 10 || width > 10000 || height < 10 || height > 10000) {
764
+ return 'Error: Width and height must be between 10 and 10000.';
765
+ }
766
+ const outputPath = String(args.output_path);
767
+ const svg = generateSVG(description, width, height);
768
+ ensureDir(outputPath);
769
+ writeFileSync(outputPath, svg);
770
+ return `Generated SVG art at ${outputPath} (${svg.length} bytes, ${width}x${height})\nOpen in a browser or SVG editor to view.`;
771
+ },
772
+ });
773
+ registerTool({
774
+ name: 'evolve_design',
775
+ description: 'Take an existing design file (HTML, SVG, GLSL, CSS, etc.) and generate N mutations by tweaking numeric values, colors, and structure. Outputs each variant as a separate file. Useful for exploring design spaces.',
776
+ parameters: {
777
+ source_path: { type: 'string', description: 'Path to the source design file to mutate', required: true },
778
+ mutations: { type: 'number', description: 'Number of mutations to generate. Defaults to 5.', default: 5 },
779
+ output_dir: { type: 'string', description: 'Directory to write mutated variants', required: true },
780
+ },
781
+ tier: 'free',
782
+ async execute(args) {
783
+ const sourcePath = String(args.source_path);
784
+ if (!existsSync(sourcePath))
785
+ return `Error: Source file not found: ${sourcePath}`;
786
+ const source = readFileSync(sourcePath, 'utf-8');
787
+ const mutations = typeof args.mutations === 'number' ? Math.max(1, Math.min(args.mutations, 50)) : 5;
788
+ const outputDir = String(args.output_dir);
789
+ mkdirSync(outputDir, { recursive: true });
790
+ const ext = extname(sourcePath);
791
+ const base = basename(sourcePath, ext);
792
+ const results = [];
793
+ for (let i = 0; i < mutations; i++) {
794
+ const mutated = evolveDesign(source, i);
795
+ const outPath = join(outputDir, `${base}_variant_${i + 1}${ext}`);
796
+ writeFileSync(outPath, mutated);
797
+ results.push(` ${outPath} (${mutated.length} bytes)`);
798
+ }
799
+ return `Evolved ${mutations} variants from ${sourcePath}:\n${results.join('\n')}\n\nEach variant has randomized tweaks to numeric values, colors, and occasionally structure.`;
800
+ },
801
+ });
802
+ }
803
+ //# sourceMappingURL=creative.js.map