@kernel.chat/kbot 2.15.2 → 2.17.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,831 @@
1
+ // K:BOT VFX & Creative Production Tools — Houdini-inspired
2
+ // Procedural generation, video processing, image manipulation,
3
+ // 3D rendering, shader generation, and creative coding.
4
+ import { registerTool } from './index.js';
5
+ import { execFile } from 'child_process';
6
+ function shell(cmd, args, timeout = 60_000) {
7
+ return new Promise((resolve, reject) => {
8
+ execFile(cmd, args, { timeout, maxBuffer: 2 * 1024 * 1024 }, (err, stdout, stderr) => {
9
+ if (err)
10
+ reject(new Error(stderr || err.message));
11
+ else
12
+ resolve(stdout || stderr);
13
+ });
14
+ });
15
+ }
16
+ export function registerVfxTools() {
17
+ // ── VEX Code Generation ───────────────────────────────────────────
18
+ registerTool({
19
+ name: 'vex_generate',
20
+ description: 'Generate Houdini VEX code for procedural effects. Creates point wrangles, volume wrangles, and attribute manipulation code.',
21
+ parameters: {
22
+ effect: { type: 'string', description: 'Effect type: noise, scatter, curl_noise, wave, fractal, vortex, erosion, growth', required: true },
23
+ target: { type: 'string', description: 'Target context: point, vertex, primitive, detail (default: point)' },
24
+ params: { type: 'string', description: 'JSON params like frequency, amplitude, octaves' },
25
+ },
26
+ tier: 'free',
27
+ async execute(args) {
28
+ const effect = String(args.effect).toLowerCase();
29
+ const target = String(args.target || 'point');
30
+ const params = args.params ? JSON.parse(String(args.params)) : {};
31
+ const templates = {
32
+ noise: `// ${target} wrangle — Perlin noise displacement
33
+ float freq = chf("frequency"); // ${params.frequency || 1.0}
34
+ float amp = chf("amplitude"); // ${params.amplitude || 0.5}
35
+ int oct = chi("octaves"); // ${params.octaves || 4}
36
+
37
+ vector pos = @P;
38
+ float n = 0;
39
+ float f = freq;
40
+ float a = amp;
41
+
42
+ for (int i = 0; i < oct; i++) {
43
+ n += a * noise(pos * f);
44
+ f *= 2.0;
45
+ a *= 0.5;
46
+ }
47
+
48
+ @P += @N * n;
49
+ @Cd = set(n, n * 0.8, n * 0.6);`,
50
+ curl_noise: `// ${target} wrangle — Curl noise for fluid-like motion
51
+ float freq = chf("frequency"); // ${params.frequency || 0.5}
52
+ float amp = chf("amplitude"); // ${params.amplitude || 1.0}
53
+ float time = @Time;
54
+
55
+ vector pos = @P * freq + time * 0.3;
56
+
57
+ // Compute curl via cross product of noise gradients
58
+ float eps = 0.001;
59
+ vector dx = set(eps, 0, 0);
60
+ vector dy = set(0, eps, 0);
61
+ vector dz = set(0, 0, eps);
62
+
63
+ float nx = noise(pos + dy).z - noise(pos - dy).z - noise(pos + dz).y + noise(pos - dz).y;
64
+ float ny = noise(pos + dz).x - noise(pos - dz).x - noise(pos + dx).z + noise(pos - dx).z;
65
+ float nz = noise(pos + dx).y - noise(pos - dx).y - noise(pos + dy).x + noise(pos - dy).x;
66
+
67
+ vector curl = set(nx, ny, nz) / (2.0 * eps);
68
+ @v = curl * amp;
69
+ @P += @v * @TimeInc;`,
70
+ scatter: `// ${target} wrangle — Poisson disk scatter
71
+ float density = chf("density"); // ${params.density || 100}
72
+ float radius = chf("min_radius"); // ${params.radius || 0.1}
73
+ float seed = chf("seed");
74
+
75
+ int npts = int(density * @Area);
76
+ for (int i = 0; i < npts; i++) {
77
+ vector2 uv = set(random(i + seed), random(i + seed + 0.5));
78
+ vector pos = primuv(0, "P", @primnum, uv);
79
+ int pt = addpoint(0, pos);
80
+ setpointattrib(0, "N", pt, @N);
81
+ setpointattrib(0, "Cd", pt, rand(set(i, seed, 0)));
82
+ }`,
83
+ wave: `// ${target} wrangle — Sine wave deformation
84
+ float freq = chf("frequency"); // ${params.frequency || 3.0}
85
+ float amp = chf("amplitude"); // ${params.amplitude || 0.3}
86
+ float speed = chf("speed"); // ${params.speed || 1.0}
87
+
88
+ float dist = length(@P.xz);
89
+ float wave = sin(dist * freq - @Time * speed) * amp;
90
+ wave *= exp(-dist * 0.1); // Falloff
91
+
92
+ @P.y += wave;
93
+ @Cd = fit01(wave / amp * 0.5 + 0.5, set(0.1, 0.2, 0.8), set(0.9, 0.95, 1.0));`,
94
+ fractal: `// ${target} wrangle — Mandelbrot fractal mapping
95
+ int max_iter = chi("iterations"); // ${params.iterations || 100}
96
+ float scale = chf("scale"); // ${params.scale || 2.0}
97
+ vector2 center = chu("center"); // ${params.center || '0, 0'}
98
+
99
+ vector2 c = (@P.xz - 0.5) * scale + set(center.x, center.y);
100
+ vector2 z = c;
101
+ int iter = 0;
102
+
103
+ for (int i = 0; i < max_iter; i++) {
104
+ if (length(z) > 2.0) break;
105
+ z = set(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;
106
+ iter++;
107
+ }
108
+
109
+ float t = float(iter) / float(max_iter);
110
+ @P.y = t * chf("height");
111
+ @Cd = chramp("color", t);`,
112
+ vortex: `// ${target} wrangle — Vortex field
113
+ float strength = chf("strength"); // ${params.strength || 2.0}
114
+ float radius = chf("radius"); // ${params.radius || 1.0}
115
+ float falloff = chf("falloff"); // ${params.falloff || 2.0}
116
+ vector center = chv("center");
117
+
118
+ vector delta = @P - center;
119
+ float dist = length(delta);
120
+ float factor = strength * exp(-pow(dist / radius, falloff));
121
+
122
+ // Tangential velocity (perpendicular to radial direction)
123
+ vector tangent = normalize(cross(delta, set(0, 1, 0)));
124
+ @v += tangent * factor;
125
+ @P += @v * @TimeInc;`,
126
+ erosion: `// ${target} wrangle — Hydraulic erosion simulation step
127
+ float sediment = 0;
128
+ float water = chf("water_amount"); // ${params.water || 0.1}
129
+ float erosion_rate = chf("erosion_rate"); // ${params.erosion_rate || 0.01}
130
+ float deposition = chf("deposition"); // ${params.deposition || 0.005}
131
+
132
+ // Get height and neighbors
133
+ float h = @P.y;
134
+ int nbs[] = neighbours(0, @ptnum);
135
+
136
+ float min_h = h;
137
+ int min_nb = -1;
138
+ foreach (int nb; nbs) {
139
+ float nh = point(0, "P", nb).y;
140
+ if (nh < min_h) { min_h = nh; min_nb = nb; }
141
+ }
142
+
143
+ if (min_nb >= 0) {
144
+ float diff = h - min_h;
145
+ float erode = min(diff * erosion_rate * water, diff * 0.5);
146
+ @P.y -= erode;
147
+ sediment += erode;
148
+ // Deposit downstream
149
+ vector npos = point(0, "P", min_nb);
150
+ npos.y += sediment * deposition;
151
+ setpointattrib(0, "P", min_nb, npos);
152
+ }`,
153
+ growth: `// ${target} wrangle — Differential growth / space colonization
154
+ float search_radius = chf("search_radius"); // ${params.search_radius || 0.5}
155
+ float step_size = chf("step_size"); // ${params.step_size || 0.05}
156
+ float repel = chf("repulsion"); // ${params.repulsion || 0.02}
157
+
158
+ int nbs[] = pcfind(0, "P", @P, search_radius, 20);
159
+ vector avg_dir = set(0, 0, 0);
160
+
161
+ foreach (int nb; nbs) {
162
+ if (nb == @ptnum) continue;
163
+ vector delta = @P - point(0, "P", nb);
164
+ float dist = length(delta);
165
+ if (dist > 0.001) {
166
+ avg_dir += normalize(delta) * repel / (dist * dist);
167
+ }
168
+ }
169
+
170
+ // Grow along normal + repulsion
171
+ @P += normalize(@N + avg_dir) * step_size;
172
+ @N = normalize(@N + avg_dir * 0.1);`,
173
+ };
174
+ const code = templates[effect];
175
+ if (!code) {
176
+ return `Unknown effect "${effect}". Available: ${Object.keys(templates).join(', ')}`;
177
+ }
178
+ return `\`\`\`vex\n${code}\n\`\`\`\n\nPaste into a ${target} wrangle in Houdini. Adjust channel references (chf/chi/chv) on the node parameters.`;
179
+ },
180
+ });
181
+ // ── FFmpeg Video Processing ───────────────────────────────────────
182
+ registerTool({
183
+ name: 'ffmpeg_process',
184
+ description: 'Process video/audio with FFmpeg. Encode, decode, filter, composite, extract frames, create timelapses, add effects.',
185
+ parameters: {
186
+ input: { type: 'string', description: 'Input file path', required: true },
187
+ output: { type: 'string', description: 'Output file path', required: true },
188
+ operation: { type: 'string', description: 'Operation: encode, extract_frames, gif, timelapse, stabilize, grayscale, reverse, speed, trim, concat, audio_extract, thumbnail', required: true },
189
+ options: { type: 'string', description: 'Additional options as JSON (e.g., {"fps": 30, "start": "00:01:00", "duration": "10"})' },
190
+ },
191
+ tier: 'free',
192
+ timeout: 300_000,
193
+ async execute(args) {
194
+ const input = String(args.input);
195
+ const output = String(args.output);
196
+ const op = String(args.operation);
197
+ const opts = args.options ? JSON.parse(String(args.options)) : {};
198
+ const commands = {
199
+ encode: ['-i', input, '-c:v', 'libx264', '-preset', 'medium', '-crf', String(opts.quality || 23), output],
200
+ extract_frames: ['-i', input, '-vf', `fps=${opts.fps || 1}`, `${output}_%04d.png`],
201
+ gif: ['-i', input, '-vf', `fps=${opts.fps || 10},scale=${opts.width || 480}:-1:flags=lanczos`, '-loop', '0', output],
202
+ timelapse: ['-i', input, '-vf', `setpts=${1 / (opts.speed || 10)}*PTS`, output],
203
+ grayscale: ['-i', input, '-vf', 'format=gray', output],
204
+ reverse: ['-i', input, '-vf', 'reverse', '-af', 'areverse', output],
205
+ speed: ['-i', input, '-vf', `setpts=${1 / (opts.speed || 2)}*PTS`, '-af', `atempo=${opts.speed || 2}`, output],
206
+ trim: ['-i', input, '-ss', opts.start || '0', '-t', opts.duration || '10', '-c', 'copy', output],
207
+ audio_extract: ['-i', input, '-vn', '-acodec', opts.codec || 'libmp3lame', output],
208
+ thumbnail: ['-i', input, '-vf', 'thumbnail', '-frames:v', '1', output],
209
+ stabilize: ['-i', input, '-vf', 'deshake', output],
210
+ };
211
+ const cmdArgs = commands[op];
212
+ if (!cmdArgs)
213
+ return `Unknown operation "${op}". Available: ${Object.keys(commands).join(', ')}`;
214
+ try {
215
+ const result = await shell('ffmpeg', ['-y', ...cmdArgs], 300_000);
216
+ return `FFmpeg ${op} complete: ${output}\n${result}`;
217
+ }
218
+ catch (err) {
219
+ return `FFmpeg error: ${err instanceof Error ? err.message : String(err)}`;
220
+ }
221
+ },
222
+ });
223
+ // ── ImageMagick ───────────────────────────────────────────────────
224
+ registerTool({
225
+ name: 'imagemagick',
226
+ description: 'Manipulate images with ImageMagick. Resize, crop, composite, effects, format conversion, batch processing.',
227
+ parameters: {
228
+ input: { type: 'string', description: 'Input image path', required: true },
229
+ output: { type: 'string', description: 'Output image path', required: true },
230
+ operation: { type: 'string', description: 'Operation: resize, crop, rotate, blur, sharpen, grayscale, sepia, posterize, edge, emboss, negate, composite, montage, border, text', required: true },
231
+ value: { type: 'string', description: 'Operation value (e.g., "50%" for resize, "10x10" for blur, text for annotation)' },
232
+ },
233
+ tier: 'free',
234
+ async execute(args) {
235
+ const input = String(args.input);
236
+ const output = String(args.output);
237
+ const op = String(args.operation);
238
+ const val = String(args.value || '');
239
+ const commands = {
240
+ resize: ['convert', input, '-resize', val || '50%', output],
241
+ crop: ['convert', input, '-crop', val || '100x100+0+0', output],
242
+ rotate: ['convert', input, '-rotate', val || '90', output],
243
+ blur: ['convert', input, '-blur', val || '0x8', output],
244
+ sharpen: ['convert', input, '-sharpen', val || '0x3', output],
245
+ grayscale: ['convert', input, '-colorspace', 'Gray', output],
246
+ sepia: ['convert', input, '-sepia-tone', val || '80%', output],
247
+ posterize: ['convert', input, '-posterize', val || '4', output],
248
+ edge: ['convert', input, '-edge', val || '1', output],
249
+ emboss: ['convert', input, '-emboss', val || '2', output],
250
+ negate: ['convert', input, '-negate', output],
251
+ border: ['convert', input, '-border', val || '10x10', '-bordercolor', '#6B5B95', output],
252
+ text: ['convert', input, '-pointsize', '36', '-fill', 'white', '-gravity', 'south', '-annotate', '+0+10', val || 'kbot', output],
253
+ };
254
+ const cmdArgs = commands[op];
255
+ if (!cmdArgs)
256
+ return `Unknown operation "${op}". Available: ${Object.keys(commands).join(', ')}`;
257
+ try {
258
+ const result = await shell('magick', cmdArgs);
259
+ return `ImageMagick ${op} complete: ${output}\n${result}`;
260
+ }
261
+ catch {
262
+ // Fallback: try without 'magick' prefix (older ImageMagick)
263
+ try {
264
+ const result = await shell(cmdArgs[0], cmdArgs.slice(1));
265
+ return `ImageMagick ${op} complete: ${output}\n${result}`;
266
+ }
267
+ catch (err) {
268
+ return `ImageMagick error: ${err instanceof Error ? err.message : String(err)}. Is ImageMagick installed?`;
269
+ }
270
+ }
271
+ },
272
+ });
273
+ // ── Blender Script ────────────────────────────────────────────────
274
+ registerTool({
275
+ name: 'blender_run',
276
+ description: 'Execute a Blender Python script in background mode. Generate 3D models, render scenes, create animations.',
277
+ parameters: {
278
+ script: { type: 'string', description: 'Python script content or file path', required: true },
279
+ output: { type: 'string', description: 'Output file path for renders' },
280
+ blend_file: { type: 'string', description: 'Optional .blend file to open first' },
281
+ },
282
+ tier: 'pro',
283
+ timeout: 300_000,
284
+ async execute(args) {
285
+ const script = String(args.script);
286
+ const output = args.output ? String(args.output) : '';
287
+ // Write inline script to temp file if it's not a path
288
+ if (!script.endsWith('.py')) {
289
+ const { writeFileSync, mkdtempSync } = await import('fs');
290
+ const { join } = await import('path');
291
+ const tmpDir = mkdtempSync('/tmp/kbot-blender-');
292
+ const scriptPath = join(tmpDir, 'script.py');
293
+ writeFileSync(scriptPath, script);
294
+ const blenderArgs = ['--background', '--python', scriptPath];
295
+ if (args.blend_file)
296
+ blenderArgs.unshift(String(args.blend_file));
297
+ if (output)
298
+ blenderArgs.push('--render-output', output, '--render-frame', '1');
299
+ try {
300
+ return await shell('blender', blenderArgs, 300_000);
301
+ }
302
+ catch (err) {
303
+ return `Blender error: ${err instanceof Error ? err.message : String(err)}. Is Blender installed?`;
304
+ }
305
+ }
306
+ const blenderArgs = ['--background', '--python', script];
307
+ if (args.blend_file)
308
+ blenderArgs.unshift(String(args.blend_file));
309
+ try {
310
+ return await shell('blender', blenderArgs, 300_000);
311
+ }
312
+ catch (err) {
313
+ return `Blender error: ${err instanceof Error ? err.message : String(err)}`;
314
+ }
315
+ },
316
+ });
317
+ // ── Procedural Texture Generation ─────────────────────────────────
318
+ registerTool({
319
+ name: 'texture_generate',
320
+ description: 'Generate tileable procedural textures using Python/Pillow. Creates noise, marble, wood, brick, hex patterns.',
321
+ parameters: {
322
+ type: { type: 'string', description: 'Texture type: perlin, marble, wood, brick, hexagon, voronoi, checkerboard, gradient', required: true },
323
+ size: { type: 'number', description: 'Texture size in pixels (default: 512)' },
324
+ output: { type: 'string', description: 'Output file path (default: texture.png)', required: true },
325
+ seed: { type: 'number', description: 'Random seed for reproducibility' },
326
+ },
327
+ tier: 'free',
328
+ async execute(args) {
329
+ const type = String(args.type);
330
+ const size = Number(args.size) || 512;
331
+ const output = String(args.output);
332
+ const seed = args.seed !== undefined ? Number(args.seed) : 42;
333
+ const script = `
334
+ import random, math
335
+ random.seed(${seed})
336
+
337
+ # Simple procedural texture generator
338
+ size = ${size}
339
+ pixels = []
340
+
341
+ def noise2d(x, y):
342
+ n = int(x * 57 + y * 131 + ${seed})
343
+ n = (n << 13) ^ n
344
+ return 1.0 - ((n * (n * n * 15731 + 789221) + 1376312589) & 0x7fffffff) / 1073741824.0
345
+
346
+ def smooth_noise(x, y):
347
+ corners = (noise2d(int(x)-1, int(y)-1) + noise2d(int(x)+1, int(y)-1) + noise2d(int(x)-1, int(y)+1) + noise2d(int(x)+1, int(y)+1)) / 16.0
348
+ sides = (noise2d(int(x)-1, int(y)) + noise2d(int(x)+1, int(y)) + noise2d(int(x), int(y)-1) + noise2d(int(x), int(y)+1)) / 8.0
349
+ center = noise2d(int(x), int(y)) / 4.0
350
+ return corners + sides + center
351
+
352
+ def lerp(a, b, t):
353
+ return a + t * (b - a)
354
+
355
+ for py in range(size):
356
+ for px in range(size):
357
+ u = px / size
358
+ v = py / size
359
+ t = '${type}'
360
+ if t == 'checkerboard':
361
+ c = int((u * 8) % 2) ^ int((v * 8) % 2)
362
+ val = c * 255
363
+ elif t == 'gradient':
364
+ val = int(u * 255)
365
+ elif t == 'hexagon':
366
+ hx = u * 8
367
+ hy = v * 8
368
+ if int(hy) % 2 == 1: hx += 0.5
369
+ fx = hx - int(hx)
370
+ fy = hy - int(hy)
371
+ d = min(abs(fx - 0.5), abs(fy - 0.5))
372
+ val = 255 if d > 0.1 else 100
373
+ elif t == 'voronoi':
374
+ min_d = 999
375
+ for i in range(16):
376
+ cx = random.Random(i + ${seed}).random()
377
+ cy = random.Random(i + ${seed} + 100).random()
378
+ d = math.sqrt((u - cx)**2 + (v - cy)**2)
379
+ min_d = min(min_d, d)
380
+ val = int(min(min_d * 500, 255))
381
+ else:
382
+ # Perlin-ish noise for perlin, marble, wood
383
+ n = smooth_noise(u * 8, v * 8)
384
+ if t == 'marble':
385
+ n = math.sin(u * 10 + n * 5) * 0.5 + 0.5
386
+ elif t == 'wood':
387
+ d = math.sqrt((u - 0.5)**2 + (v - 0.5)**2) * 20
388
+ n = math.sin(d + n * 2) * 0.5 + 0.5
389
+ else:
390
+ n = n * 0.5 + 0.5
391
+ val = int(max(0, min(255, n * 255)))
392
+ pixels.append(val)
393
+
394
+ # Write as PGM then convert
395
+ with open('${output.replace(/'/g, "\\'")}', 'wb') as f:
396
+ header = f'P5\\n${size} ${size}\\n255\\n'
397
+ f.write(header.encode())
398
+ f.write(bytes(pixels))
399
+
400
+ print(f'Generated ${type} texture: ${size}x${size} -> ${output}')
401
+ `;
402
+ try {
403
+ return await shell('python3', ['-c', script], 30_000);
404
+ }
405
+ catch (err) {
406
+ return `Texture generation error: ${err instanceof Error ? err.message : String(err)}`;
407
+ }
408
+ },
409
+ });
410
+ // ── GLSL Shader Generation ────────────────────────────────────────
411
+ registerTool({
412
+ name: 'shader_generate',
413
+ description: 'Generate GLSL/HLSL shader code from descriptions. Creates vertex, fragment, compute shaders for effects like water, fire, displacement, post-processing.',
414
+ parameters: {
415
+ effect: { type: 'string', description: 'Shader effect: water, fire, plasma, raymarching, fog, bloom, chromatic_aberration, film_grain, outline, dissolve', required: true },
416
+ language: { type: 'string', description: 'Shader language: glsl, hlsl, wgsl (default: glsl)' },
417
+ },
418
+ tier: 'free',
419
+ async execute(args) {
420
+ const effect = String(args.effect).toLowerCase();
421
+ const lang = String(args.language || 'glsl').toLowerCase();
422
+ const shaders = {
423
+ water: `// Water surface shader — ${lang.toUpperCase()}
424
+ precision mediump float;
425
+ uniform float u_time;
426
+ uniform vec2 u_resolution;
427
+
428
+ float wave(vec2 p, float t) {
429
+ return sin(p.x * 3.0 + t) * 0.1 +
430
+ sin(p.y * 4.0 + t * 1.3) * 0.08 +
431
+ sin((p.x + p.y) * 5.0 + t * 0.7) * 0.05;
432
+ }
433
+
434
+ void main() {
435
+ vec2 uv = gl_FragCoord.xy / u_resolution;
436
+ float w = wave(uv * 10.0, u_time);
437
+
438
+ // Fresnel-like edge darkening
439
+ float depth = 0.3 + w;
440
+ vec3 shallow = vec3(0.2, 0.7, 0.9);
441
+ vec3 deep = vec3(0.0, 0.1, 0.3);
442
+ vec3 color = mix(deep, shallow, depth);
443
+
444
+ // Specular highlight
445
+ float spec = pow(max(0.0, w * 5.0), 8.0) * 0.5;
446
+ color += vec3(spec);
447
+
448
+ gl_FragColor = vec4(color, 0.9);
449
+ }`,
450
+ fire: `// Procedural fire shader — ${lang.toUpperCase()}
451
+ precision mediump float;
452
+ uniform float u_time;
453
+ uniform vec2 u_resolution;
454
+
455
+ float hash(vec2 p) {
456
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
457
+ }
458
+
459
+ float noise(vec2 p) {
460
+ vec2 i = floor(p);
461
+ vec2 f = fract(p);
462
+ f = f * f * (3.0 - 2.0 * f);
463
+ return mix(
464
+ mix(hash(i), hash(i + vec2(1,0)), f.x),
465
+ mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), f.x),
466
+ f.y
467
+ );
468
+ }
469
+
470
+ float fbm(vec2 p) {
471
+ float v = 0.0, a = 0.5;
472
+ for (int i = 0; i < 5; i++) {
473
+ v += a * noise(p);
474
+ p *= 2.0;
475
+ a *= 0.5;
476
+ }
477
+ return v;
478
+ }
479
+
480
+ void main() {
481
+ vec2 uv = gl_FragCoord.xy / u_resolution;
482
+ uv.y = 1.0 - uv.y; // Flip Y
483
+
484
+ float n = fbm(uv * 5.0 + vec2(0, -u_time * 2.0));
485
+ float shape = 1.0 - uv.y;
486
+ shape *= smoothstep(0.0, 0.3, 0.5 - abs(uv.x - 0.5));
487
+
488
+ float fire = shape * n * 2.0;
489
+
490
+ vec3 col = vec3(1.5, 0.5, 0.1) * fire;
491
+ col += vec3(1.0, 0.9, 0.3) * pow(fire, 3.0);
492
+ col += vec3(0.3, 0.05, 0.0) * smoothstep(0.0, 0.5, fire);
493
+
494
+ gl_FragColor = vec4(col, fire);
495
+ }`,
496
+ plasma: `// Plasma effect shader — ${lang.toUpperCase()}
497
+ precision mediump float;
498
+ uniform float u_time;
499
+ uniform vec2 u_resolution;
500
+
501
+ void main() {
502
+ vec2 uv = gl_FragCoord.xy / u_resolution * 10.0;
503
+ float t = u_time;
504
+
505
+ float v = sin(uv.x + t);
506
+ v += sin((uv.y + t) * 0.5);
507
+ v += sin((uv.x + uv.y + t) * 0.5);
508
+ v += sin(sqrt(uv.x * uv.x + uv.y * uv.y) + t);
509
+ v *= 0.5;
510
+
511
+ vec3 color = vec3(
512
+ sin(v * 3.14159) * 0.5 + 0.5,
513
+ sin(v * 3.14159 + 2.094) * 0.5 + 0.5,
514
+ sin(v * 3.14159 + 4.189) * 0.5 + 0.5
515
+ );
516
+
517
+ gl_FragColor = vec4(color, 1.0);
518
+ }`,
519
+ raymarching: `// Raymarching SDF shader — ${lang.toUpperCase()}
520
+ precision mediump float;
521
+ uniform float u_time;
522
+ uniform vec2 u_resolution;
523
+
524
+ float sdSphere(vec3 p, float r) { return length(p) - r; }
525
+ 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)); }
526
+
527
+ float scene(vec3 p) {
528
+ float sphere = sdSphere(p - vec3(0, 0, 0), 1.0);
529
+ float box = sdBox(p - vec3(0, 0, 0), vec3(0.75));
530
+ float blend = mix(sphere, box, sin(u_time) * 0.5 + 0.5);
531
+
532
+ // Infinite repetition
533
+ vec3 q = mod(p + 2.0, 4.0) - 2.0;
534
+ float repeated = sdSphere(q, 0.3);
535
+
536
+ return min(blend, repeated);
537
+ }
538
+
539
+ void main() {
540
+ vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.y;
541
+ vec3 ro = vec3(0, 0, -3);
542
+ vec3 rd = normalize(vec3(uv, 1));
543
+
544
+ float t = 0.0;
545
+ for (int i = 0; i < 64; i++) {
546
+ vec3 p = ro + rd * t;
547
+ float d = scene(p);
548
+ if (d < 0.001 || t > 20.0) break;
549
+ t += d;
550
+ }
551
+
552
+ vec3 col = vec3(0);
553
+ if (t < 20.0) {
554
+ vec3 p = ro + rd * t;
555
+ // Simple normal estimation
556
+ vec2 e = vec2(0.001, 0);
557
+ vec3 n = normalize(vec3(
558
+ scene(p + e.xyy) - scene(p - e.xyy),
559
+ scene(p + e.yxy) - scene(p - e.yxy),
560
+ scene(p + e.yyx) - scene(p - e.yyx)
561
+ ));
562
+ float diff = max(dot(n, normalize(vec3(1, 1, -1))), 0.0);
563
+ col = vec3(0.4, 0.2, 0.6) * diff + vec3(0.1);
564
+ }
565
+
566
+ gl_FragColor = vec4(col, 1.0);
567
+ }`,
568
+ bloom: `// Post-processing bloom shader — ${lang.toUpperCase()}
569
+ precision mediump float;
570
+ uniform sampler2D u_texture;
571
+ uniform vec2 u_resolution;
572
+ uniform float u_threshold;
573
+ uniform float u_intensity;
574
+
575
+ vec3 sampleBlur(vec2 uv, float radius) {
576
+ vec3 sum = vec3(0);
577
+ float total = 0.0;
578
+ for (float x = -4.0; x <= 4.0; x += 1.0) {
579
+ for (float y = -4.0; y <= 4.0; y += 1.0) {
580
+ vec2 offset = vec2(x, y) * radius / u_resolution;
581
+ float weight = 1.0 - length(vec2(x, y)) / 5.66;
582
+ if (weight > 0.0) {
583
+ sum += texture2D(u_texture, uv + offset).rgb * weight;
584
+ total += weight;
585
+ }
586
+ }
587
+ }
588
+ return sum / total;
589
+ }
590
+
591
+ void main() {
592
+ vec2 uv = gl_FragCoord.xy / u_resolution;
593
+ vec3 color = texture2D(u_texture, uv).rgb;
594
+
595
+ // Extract bright areas
596
+ float brightness = dot(color, vec3(0.2126, 0.7152, 0.0722));
597
+ vec3 bright = color * step(u_threshold, brightness);
598
+
599
+ // Blur bright areas
600
+ vec3 bloom = sampleBlur(uv, 3.0);
601
+
602
+ gl_FragColor = vec4(color + bloom * u_intensity, 1.0);
603
+ }`,
604
+ film_grain: `// Film grain post-processing — ${lang.toUpperCase()}
605
+ precision mediump float;
606
+ uniform sampler2D u_texture;
607
+ uniform vec2 u_resolution;
608
+ uniform float u_time;
609
+ uniform float u_intensity;
610
+
611
+ float rand(vec2 co) {
612
+ return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
613
+ }
614
+
615
+ void main() {
616
+ vec2 uv = gl_FragCoord.xy / u_resolution;
617
+ vec3 color = texture2D(u_texture, uv).rgb;
618
+
619
+ float grain = rand(uv + fract(u_time)) * 2.0 - 1.0;
620
+ grain *= u_intensity;
621
+
622
+ // Vignette
623
+ float vignette = 1.0 - length(uv - 0.5) * 1.2;
624
+ vignette = smoothstep(0.0, 1.0, vignette);
625
+
626
+ color += grain;
627
+ color *= vignette;
628
+
629
+ gl_FragColor = vec4(color, 1.0);
630
+ }`,
631
+ dissolve: `// Dissolve transition shader — ${lang.toUpperCase()}
632
+ precision mediump float;
633
+ uniform sampler2D u_texture;
634
+ uniform vec2 u_resolution;
635
+ uniform float u_progress;
636
+
637
+ float hash(vec2 p) {
638
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
639
+ }
640
+
641
+ void main() {
642
+ vec2 uv = gl_FragCoord.xy / u_resolution;
643
+ vec3 color = texture2D(u_texture, uv).rgb;
644
+
645
+ float noise = hash(uv * 100.0);
646
+ float edge = smoothstep(u_progress - 0.05, u_progress + 0.05, noise);
647
+
648
+ // Glowing edge
649
+ float edgeGlow = 1.0 - abs(noise - u_progress) * 20.0;
650
+ edgeGlow = max(0.0, edgeGlow);
651
+ vec3 glowColor = vec3(1.0, 0.5, 0.1) * edgeGlow * 3.0;
652
+
653
+ color = mix(vec3(0), color, edge) + glowColor;
654
+ float alpha = step(0.01, edge + edgeGlow);
655
+
656
+ gl_FragColor = vec4(color, alpha);
657
+ }`,
658
+ };
659
+ const code = shaders[effect];
660
+ if (!code)
661
+ return `Unknown shader "${effect}". Available: ${Object.keys(shaders).join(', ')}`;
662
+ return `\`\`\`glsl\n${code}\n\`\`\``;
663
+ },
664
+ });
665
+ // ── Color Palette Generator ───────────────────────────────────────
666
+ registerTool({
667
+ name: 'color_palette',
668
+ description: 'Generate color palettes from descriptions, images, or color theory rules. Returns hex colors with names.',
669
+ parameters: {
670
+ source: { type: 'string', description: 'Description ("warm sunset"), hex color ("#6B5B95"), or image path', required: true },
671
+ count: { type: 'number', description: 'Number of colors (default: 5)' },
672
+ harmony: { type: 'string', description: 'Color harmony: complementary, analogous, triadic, split_complementary, tetradic, monochromatic' },
673
+ },
674
+ tier: 'free',
675
+ async execute(args) {
676
+ const source = String(args.source);
677
+ const count = Number(args.count) || 5;
678
+ // If source is a hex color, generate harmonies
679
+ if (source.startsWith('#') && source.length >= 7) {
680
+ const r = parseInt(source.slice(1, 3), 16);
681
+ const g = parseInt(source.slice(3, 5), 16);
682
+ const b = parseInt(source.slice(5, 7), 16);
683
+ // Convert to HSL
684
+ const rf = r / 255, gf = g / 255, bf = b / 255;
685
+ const max = Math.max(rf, gf, bf), min = Math.min(rf, gf, bf);
686
+ let h = 0, s = 0;
687
+ const l = (max + min) / 2;
688
+ if (max !== min) {
689
+ const d = max - min;
690
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
691
+ if (max === rf)
692
+ h = ((gf - bf) / d + (gf < bf ? 6 : 0)) / 6;
693
+ else if (max === gf)
694
+ h = ((bf - rf) / d + 2) / 6;
695
+ else
696
+ h = ((rf - gf) / d + 4) / 6;
697
+ }
698
+ const harmony = String(args.harmony || 'analogous');
699
+ const offsets = [];
700
+ switch (harmony) {
701
+ case 'complementary':
702
+ offsets.push(0, 0.5);
703
+ break;
704
+ case 'triadic':
705
+ offsets.push(0, 1 / 3, 2 / 3);
706
+ break;
707
+ case 'split_complementary':
708
+ offsets.push(0, 5 / 12, 7 / 12);
709
+ break;
710
+ case 'tetradic':
711
+ offsets.push(0, 0.25, 0.5, 0.75);
712
+ break;
713
+ case 'monochromatic':
714
+ for (let i = 0; i < count; i++)
715
+ offsets.push(0);
716
+ break;
717
+ default:
718
+ for (let i = 0; i < count; i++)
719
+ offsets.push(i * 30 / 360);
720
+ break; // analogous
721
+ }
722
+ // Pad to requested count
723
+ while (offsets.length < count)
724
+ offsets.push(offsets[offsets.length - 1] + 0.1);
725
+ const hslToHex = (h, s, l) => {
726
+ h = ((h % 1) + 1) % 1;
727
+ const hue2rgb = (p, q, t) => {
728
+ if (t < 0)
729
+ t += 1;
730
+ if (t > 1)
731
+ t -= 1;
732
+ if (t < 1 / 6)
733
+ return p + (q - p) * 6 * t;
734
+ if (t < 1 / 2)
735
+ return q;
736
+ if (t < 2 / 3)
737
+ return p + (q - p) * (2 / 3 - t) * 6;
738
+ return p;
739
+ };
740
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
741
+ const p = 2 * l - q;
742
+ const ri = Math.round(hue2rgb(p, q, h + 1 / 3) * 255);
743
+ const gi = Math.round(hue2rgb(p, q, h) * 255);
744
+ const bi = Math.round(hue2rgb(p, q, h - 1 / 3) * 255);
745
+ return `#${ri.toString(16).padStart(2, '0')}${gi.toString(16).padStart(2, '0')}${bi.toString(16).padStart(2, '0')}`;
746
+ };
747
+ const colors = offsets.slice(0, count).map((offset, i) => {
748
+ const newH = h + offset;
749
+ const newL = harmony === 'monochromatic' ? l - 0.15 + (i * 0.3 / count) : l;
750
+ return hslToHex(newH, s, Math.max(0.1, Math.min(0.9, newL)));
751
+ });
752
+ return `## ${harmony.replace('_', ' ')} palette from ${source}\n\n${colors.map((c, i) => `${i + 1}. \`${c}\` ${'█'.repeat(8)}`).join('\n')}`;
753
+ }
754
+ // Named palette descriptions
755
+ const palettes = {
756
+ 'warm sunset': ['#FF6B35', '#F7C59F', '#EFEFD0', '#004E7C', '#1A2238'],
757
+ 'ocean': ['#05445E', '#189AB4', '#75E6DA', '#D4F1F9', '#E8F8F5'],
758
+ 'forest': ['#2D5016', '#4A7C2E', '#8FBC54', '#C5E17A', '#F0F4E4'],
759
+ 'cyberpunk': ['#FF00FF', '#00FFFF', '#FF006E', '#8338EC', '#3A0CA3'],
760
+ 'minimal': ['#2B2D42', '#8D99AE', '#EDF2F4', '#EF233C', '#D90429'],
761
+ 'earth': ['#5C4033', '#8B6914', '#DAA520', '#F0E68C', '#FAF0E6'],
762
+ 'pastel': ['#FFB3BA', '#FFDFBA', '#FFFFBA', '#BAFFC9', '#BAE1FF'],
763
+ 'midnight': ['#0D1B2A', '#1B2838', '#324A5F', '#6B8F71', '#AAC0AA'],
764
+ 'rubin': ['#6B5B95', '#8E7CC3', '#B8A9C9', '#F5F0EB', '#2A2A2A'],
765
+ };
766
+ const key = Object.keys(palettes).find(k => source.toLowerCase().includes(k));
767
+ if (key) {
768
+ const colors = palettes[key].slice(0, count);
769
+ return `## "${key}" palette\n\n${colors.map((c, i) => `${i + 1}. \`${c}\` ${'█'.repeat(8)}`).join('\n')}`;
770
+ }
771
+ // Default: generate based on hash of description
772
+ const hash = Array.from(source).reduce((acc, c) => acc + c.charCodeAt(0), 0);
773
+ const baseHue = (hash % 360) / 360;
774
+ const colors = Array.from({ length: count }, (_, i) => {
775
+ const h = baseHue + i * (1 / count);
776
+ const s = 0.5 + (i % 2) * 0.2;
777
+ const l = 0.3 + i * (0.4 / count);
778
+ const hslToHex2 = (h, s, l) => {
779
+ h = ((h % 1) + 1) % 1;
780
+ const a = s * Math.min(l, 1 - l);
781
+ const f = (n) => {
782
+ const k = (n + h * 12) % 12;
783
+ return Math.round((l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)) * 255);
784
+ };
785
+ return `#${f(0).toString(16).padStart(2, '0')}${f(8).toString(16).padStart(2, '0')}${f(4).toString(16).padStart(2, '0')}`;
786
+ };
787
+ return hslToHex2(h, s, l);
788
+ });
789
+ return `## Palette for "${source}"\n\n${colors.map((c, i) => `${i + 1}. \`${c}\` ${'█'.repeat(8)}`).join('\n')}`;
790
+ },
791
+ });
792
+ // ── Audio Visualization ───────────────────────────────────────────
793
+ registerTool({
794
+ name: 'audio_visualize',
795
+ description: 'Generate audio visualization videos from audio files using FFmpeg. Creates waveform, spectrum, or vectorscope visualizations.',
796
+ parameters: {
797
+ input: { type: 'string', description: 'Input audio file path', required: true },
798
+ output: { type: 'string', description: 'Output video file path', required: true },
799
+ style: { type: 'string', description: 'Visualization style: waveform, spectrum, vectorscope, showcqt (default: showcqt)' },
800
+ size: { type: 'string', description: 'Video size (default: 1920x1080)' },
801
+ duration: { type: 'string', description: 'Duration in seconds (default: full audio)' },
802
+ },
803
+ tier: 'pro',
804
+ timeout: 300_000,
805
+ async execute(args) {
806
+ const input = String(args.input);
807
+ const output = String(args.output);
808
+ const style = String(args.style || 'showcqt');
809
+ const size = String(args.size || '1920x1080');
810
+ const filters = {
811
+ waveform: `showwaves=s=${size}:mode=cline:colors=0x6B5B95|0x8E7CC3`,
812
+ spectrum: `showspectrum=s=${size}:mode=combined:color=intensity:scale=cbrt`,
813
+ vectorscope: `avectorscope=s=${size}:mode=lissajous_xy:zoom=5`,
814
+ showcqt: `showcqt=s=${size}:sono_h=0:bar_h=${size.split('x')[1]}:sono_g=4:bar_g=4`,
815
+ };
816
+ const filter = filters[style];
817
+ if (!filter)
818
+ return `Unknown style "${style}". Available: ${Object.keys(filters).join(', ')}`;
819
+ const ffmpegArgs = ['-y', '-i', input, '-filter_complex', filter, '-pix_fmt', 'yuv420p', output];
820
+ if (args.duration)
821
+ ffmpegArgs.splice(3, 0, '-t', String(args.duration));
822
+ try {
823
+ return await shell('ffmpeg', ffmpegArgs, 300_000);
824
+ }
825
+ catch (err) {
826
+ return `Audio visualization error: ${err instanceof Error ? err.message : String(err)}`;
827
+ }
828
+ },
829
+ });
830
+ }
831
+ //# sourceMappingURL=vfx.js.map