vizcore 0.1.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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +170 -0
  4. data/docs/GETTING_STARTED.md +105 -0
  5. data/examples/assets/complex_demo_loop.wav +0 -0
  6. data/examples/basic.rb +9 -0
  7. data/examples/complex_audio_showcase.rb +261 -0
  8. data/examples/custom_shader.rb +21 -0
  9. data/examples/file_audio_demo.rb +74 -0
  10. data/examples/intro_drop.rb +38 -0
  11. data/examples/midi_scene_switch.rb +32 -0
  12. data/examples/shaders/custom_wave.frag +30 -0
  13. data/exe/vizcore +6 -0
  14. data/frontend/index.html +148 -0
  15. data/frontend/src/main.js +304 -0
  16. data/frontend/src/renderer/engine.js +135 -0
  17. data/frontend/src/renderer/layer-manager.js +456 -0
  18. data/frontend/src/renderer/shader-manager.js +69 -0
  19. data/frontend/src/shaders/builtins.js +244 -0
  20. data/frontend/src/shaders/post-effects.js +85 -0
  21. data/frontend/src/visuals/geometry.js +66 -0
  22. data/frontend/src/visuals/particle-system.js +148 -0
  23. data/frontend/src/visuals/text-renderer.js +143 -0
  24. data/frontend/src/visuals/vj-effects.js +56 -0
  25. data/frontend/src/websocket-client.js +131 -0
  26. data/lib/vizcore/analysis/band_splitter.rb +63 -0
  27. data/lib/vizcore/analysis/beat_detector.rb +70 -0
  28. data/lib/vizcore/analysis/bpm_estimator.rb +86 -0
  29. data/lib/vizcore/analysis/fft_processor.rb +224 -0
  30. data/lib/vizcore/analysis/fftw_ffi.rb +50 -0
  31. data/lib/vizcore/analysis/pipeline.rb +72 -0
  32. data/lib/vizcore/analysis/smoother.rb +74 -0
  33. data/lib/vizcore/analysis.rb +14 -0
  34. data/lib/vizcore/audio/base_input.rb +39 -0
  35. data/lib/vizcore/audio/dummy_sine_input.rb +40 -0
  36. data/lib/vizcore/audio/file_input.rb +163 -0
  37. data/lib/vizcore/audio/input_manager.rb +133 -0
  38. data/lib/vizcore/audio/mic_input.rb +121 -0
  39. data/lib/vizcore/audio/midi_input.rb +246 -0
  40. data/lib/vizcore/audio/portaudio_ffi.rb +243 -0
  41. data/lib/vizcore/audio/ring_buffer.rb +92 -0
  42. data/lib/vizcore/audio.rb +16 -0
  43. data/lib/vizcore/cli.rb +115 -0
  44. data/lib/vizcore/config.rb +46 -0
  45. data/lib/vizcore/dsl/engine.rb +229 -0
  46. data/lib/vizcore/dsl/file_watcher.rb +108 -0
  47. data/lib/vizcore/dsl/layer_builder.rb +182 -0
  48. data/lib/vizcore/dsl/mapping_resolver.rb +81 -0
  49. data/lib/vizcore/dsl/midi_map_executor.rb +188 -0
  50. data/lib/vizcore/dsl/scene_builder.rb +44 -0
  51. data/lib/vizcore/dsl/shader_source_resolver.rb +71 -0
  52. data/lib/vizcore/dsl/transition_controller.rb +166 -0
  53. data/lib/vizcore/dsl.rb +16 -0
  54. data/lib/vizcore/errors.rb +27 -0
  55. data/lib/vizcore/renderer/frame_scheduler.rb +75 -0
  56. data/lib/vizcore/renderer/scene_serializer.rb +73 -0
  57. data/lib/vizcore/renderer.rb +10 -0
  58. data/lib/vizcore/server/frame_broadcaster.rb +351 -0
  59. data/lib/vizcore/server/rack_app.rb +183 -0
  60. data/lib/vizcore/server/runner.rb +357 -0
  61. data/lib/vizcore/server/websocket_handler.rb +163 -0
  62. data/lib/vizcore/server.rb +12 -0
  63. data/lib/vizcore/templates/basic_scene.rb +10 -0
  64. data/lib/vizcore/templates/custom_shader_scene.rb +22 -0
  65. data/lib/vizcore/templates/custom_wave.frag +31 -0
  66. data/lib/vizcore/templates/intro_drop_scene.rb +40 -0
  67. data/lib/vizcore/templates/midi_control_scene.rb +33 -0
  68. data/lib/vizcore/templates/project_readme.md +35 -0
  69. data/lib/vizcore/version.rb +6 -0
  70. data/lib/vizcore.rb +37 -0
  71. metadata +186 -0
@@ -0,0 +1,456 @@
1
+ import { getBuiltinShader } from "../shaders/builtins.js";
2
+ import { getPostEffectShader } from "../shaders/post-effects.js";
3
+ import { buildWireframeLines, estimateDeformFromSpectrum } from "../visuals/geometry.js";
4
+ import { ParticleSystem } from "../visuals/particle-system.js";
5
+ import { TextRenderer } from "../visuals/text-renderer.js";
6
+ import { getVJEffectShader } from "../visuals/vj-effects.js";
7
+ import { FULLSCREEN_VERTEX_SHADER } from "./shader-manager.js";
8
+
9
+ const GEOMETRY_VERTEX_SHADER = `#version 300 es
10
+ in vec2 a_position;
11
+ void main() {
12
+ gl_Position = vec4(a_position, 0.0, 1.0);
13
+ }
14
+ `;
15
+
16
+ const GEOMETRY_FRAGMENT_SHADER = `#version 300 es
17
+ precision mediump float;
18
+ uniform vec3 u_color;
19
+ out vec4 outColor;
20
+ void main() {
21
+ outColor = vec4(u_color, 1.0);
22
+ }
23
+ `;
24
+
25
+ const COMPOSITE_FRAGMENT_SHADER = `#version 300 es
26
+ precision mediump float;
27
+ in vec2 v_uv;
28
+ uniform sampler2D u_texture;
29
+ uniform float u_opacity;
30
+ out vec4 outColor;
31
+ void main() {
32
+ vec4 color = texture(u_texture, v_uv);
33
+ outColor = vec4(color.rgb, color.a * u_opacity);
34
+ }
35
+ `;
36
+
37
+ const FULLSCREEN_VERTICES = new Float32Array([
38
+ -1.0, -1.0,
39
+ 1.0, -1.0,
40
+ -1.0, 1.0,
41
+ 1.0, 1.0
42
+ ]);
43
+ const MAX_LAYER_TARGET_PIXELS = 4_194_304;
44
+
45
+ export class LayerManager {
46
+ constructor(gl, shaderManager) {
47
+ this.gl = gl;
48
+ this.shaderManager = shaderManager;
49
+
50
+ this.fullscreenBuffer = this.gl.createBuffer();
51
+ this.geometryBuffer = this.gl.createBuffer();
52
+
53
+ this.geometryProgram = this.shaderManager.getProgram(
54
+ "geometry-wireframe",
55
+ GEOMETRY_VERTEX_SHADER,
56
+ GEOMETRY_FRAGMENT_SHADER
57
+ );
58
+ this.geometryPositionLocation = this.gl.getAttribLocation(this.geometryProgram, "a_position");
59
+ this.geometryColorLocation = this.gl.getUniformLocation(this.geometryProgram, "u_color");
60
+
61
+ this.compositeProgram = this.shaderManager.getProgram(
62
+ "layer-composite",
63
+ FULLSCREEN_VERTEX_SHADER,
64
+ COMPOSITE_FRAGMENT_SHADER
65
+ );
66
+ this.compositePositionLocation = this.gl.getAttribLocation(this.compositeProgram, "a_position");
67
+ this.compositeTextureLocation = this.gl.getUniformLocation(this.compositeProgram, "u_texture");
68
+ this.compositeOpacityLocation = this.gl.getUniformLocation(this.compositeProgram, "u_opacity");
69
+
70
+ this.layerFramebuffer = null;
71
+ this.layerTexture = null;
72
+ this.layerDepthRenderbuffer = null;
73
+ this.layerTargetWidth = 0;
74
+ this.layerTargetHeight = 0;
75
+ this.layerTargetAvailable = true;
76
+ this.layerErrorKeys = new Set();
77
+
78
+ this.particleSystem = new ParticleSystem(this.gl, this.shaderManager);
79
+ this.textRenderer = new TextRenderer(this.gl, this.shaderManager);
80
+
81
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.fullscreenBuffer);
82
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, FULLSCREEN_VERTICES, this.gl.STATIC_DRAW);
83
+ }
84
+
85
+ renderScene({ layers, audio, time, rotation, resolution }) {
86
+ const layerList = Array.isArray(layers) && layers.length > 0 ? layers : [defaultLayer(audio)];
87
+ const width = Math.max(1, Math.floor(Number(resolution?.[0] || 1)));
88
+ const height = Math.max(1, Math.floor(Number(resolution?.[1] || 1)));
89
+ this.ensureLayerTarget(width, height);
90
+
91
+ if (!this.layerTargetAvailable || !this.layerFramebuffer || !this.layerTexture) {
92
+ for (const layer of layerList) {
93
+ try {
94
+ const blend = String(layer?.params?.blend || "alpha").toLowerCase();
95
+ this.setBlendMode(blend);
96
+ this.renderLayer(layer, audio, time, rotation, [width, height]);
97
+ } catch (error) {
98
+ this.reportLayerError(layer, error, "direct-render");
99
+ }
100
+ }
101
+ this.setBlendMode("alpha");
102
+ return;
103
+ }
104
+
105
+ for (const layer of layerList) {
106
+ try {
107
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.layerFramebuffer);
108
+ this.gl.viewport(0, 0, this.layerTargetWidth, this.layerTargetHeight);
109
+ this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
110
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
111
+
112
+ this.renderLayer(layer, audio, time, rotation, [this.layerTargetWidth, this.layerTargetHeight]);
113
+
114
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
115
+ this.gl.viewport(0, 0, width, height);
116
+ this.compositeLayer(layer, { audio, time, resolution: [width, height] });
117
+ } catch (error) {
118
+ this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
119
+ this.gl.viewport(0, 0, width, height);
120
+ this.reportLayerError(layer, error, "layer-pass");
121
+ }
122
+ }
123
+ this.setBlendMode("alpha");
124
+ }
125
+
126
+ renderLayer(layer, audio, time, rotation, resolution) {
127
+ if (isParticleLayer(layer)) {
128
+ this.renderParticleLayer(layer, audio, time);
129
+ return;
130
+ }
131
+ if (isTextLayer(layer)) {
132
+ this.renderTextLayer(layer, audio, time);
133
+ return;
134
+ }
135
+ if (isShaderLayer(layer)) {
136
+ this.renderShaderLayer(layer, audio, time, resolution);
137
+ return;
138
+ }
139
+ this.renderGeometryLayer(layer, audio, rotation);
140
+ }
141
+
142
+ renderShaderLayer(layer, audio, time, resolution) {
143
+ const shaderName = String(layer?.shader || "gradient_pulse");
144
+ const customSource = typeof layer?.glsl_source === "string" ? layer.glsl_source : null;
145
+ const fragmentShader = customSource || getBuiltinShader(shaderName);
146
+ const cacheKey = customSource
147
+ ? `custom:${String(layer?.glsl || shaderName)}:${hashString(customSource)}`
148
+ : `builtin:${shaderName}`;
149
+ let program = null;
150
+ try {
151
+ program = this.shaderManager.getProgram(cacheKey, FULLSCREEN_VERTEX_SHADER, fragmentShader);
152
+ } catch (error) {
153
+ if (customSource) {
154
+ console.warn("Failed to compile custom GLSL, falling back to builtin shader", error);
155
+ try {
156
+ program = this.shaderManager.getProgram(
157
+ `builtin:${shaderName}`,
158
+ FULLSCREEN_VERTEX_SHADER,
159
+ getBuiltinShader(shaderName)
160
+ );
161
+ } catch (builtinError) {
162
+ this.reportLayerError(layer, builtinError, "builtin-shader-fallback");
163
+ }
164
+ } else {
165
+ this.reportLayerError(layer, error, "builtin-shader");
166
+ }
167
+
168
+ if (!program) {
169
+ program = this.shaderManager.getProgram(
170
+ "builtin:default",
171
+ FULLSCREEN_VERTEX_SHADER,
172
+ getBuiltinShader("default")
173
+ );
174
+ }
175
+ }
176
+ const gl = this.gl;
177
+
178
+ gl.useProgram(program);
179
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.fullscreenBuffer);
180
+
181
+ const positionLocation = gl.getAttribLocation(program, "a_position");
182
+ gl.enableVertexAttribArray(positionLocation);
183
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
184
+
185
+ const bands = audio?.bands || {};
186
+ this.setUniform1f(program, "u_time", time);
187
+ this.setUniform2f(program, "u_resolution", resolution[0], resolution[1]);
188
+ this.setUniform1f(program, "u_amplitude", audio?.amplitude || 0);
189
+ this.setUniform1f(program, "u_bass", bands.low || 0);
190
+ this.setUniform1f(program, "u_mid", bands.mid || 0);
191
+ this.setUniform1f(program, "u_high", bands.high || 0);
192
+ this.setUniform1f(program, "u_beat", audio?.beat ? 1 : 0);
193
+ this.setUniform1f(program, "u_bpm", audio?.bpm || 0);
194
+
195
+ const params = layer?.params || {};
196
+ for (const [key, value] of Object.entries(params)) {
197
+ if (typeof value !== "number") {
198
+ continue;
199
+ }
200
+ const safeKey = key.replace(/[^a-zA-Z0-9_]/g, "_");
201
+ this.setUniform1f(program, `u_param_${safeKey}`, value);
202
+ }
203
+
204
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
205
+ }
206
+
207
+ renderGeometryLayer(layer, audio, rotation) {
208
+ const gl = this.gl;
209
+ const params = layer?.params || {};
210
+ const colorShift = clamp(Number(params.color_shift || 0), 0, 1);
211
+ const deform = estimateDeformFromSpectrum(params.deform ?? audio?.fft);
212
+ const points = buildWireframeLines({
213
+ rotationY: rotation,
214
+ rotationX: rotation * 0.8,
215
+ deform
216
+ });
217
+
218
+ gl.useProgram(this.geometryProgram);
219
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.geometryBuffer);
220
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.DYNAMIC_DRAW);
221
+ gl.enableVertexAttribArray(this.geometryPositionLocation);
222
+ gl.vertexAttribPointer(this.geometryPositionLocation, 2, gl.FLOAT, false, 0, 0);
223
+
224
+ const amplitude = clamp(Number(audio?.amplitude || 0), 0, 1);
225
+ gl.uniform3f(
226
+ this.geometryColorLocation,
227
+ 0.45 + amplitude * 0.45,
228
+ 0.75 + colorShift * 0.2,
229
+ 0.96
230
+ );
231
+ gl.drawArrays(gl.LINES, 0, points.length / 2);
232
+ }
233
+
234
+ renderParticleLayer(layer, audio, time) {
235
+ const params = layer?.params || {};
236
+ this.particleSystem.render({
237
+ count: Number(params.count || 2400),
238
+ speed: Number(params.speed || audio?.amplitude || 0),
239
+ size: Number(params.size || 2.0),
240
+ audio,
241
+ time
242
+ });
243
+ }
244
+
245
+ renderTextLayer(layer, audio, time) {
246
+ const params = layer?.params || {};
247
+ this.textRenderer.render({
248
+ content: params.content || "VIZCORE",
249
+ fontSize: Number(params.font_size || 120),
250
+ color: params.color || "#e5f3ff",
251
+ glowStrength: Number(params.glow_strength ?? 0.15),
252
+ audio,
253
+ time
254
+ });
255
+ }
256
+
257
+ compositeLayer(layer, { audio, time, resolution }) {
258
+ const gl = this.gl;
259
+ const params = layer?.params || {};
260
+ const opacity = clamp(Number(params.opacity || 1), 0, 1);
261
+ const blend = String(params.blend || "alpha").toLowerCase();
262
+ const effectName = String(params.effect || "");
263
+ const vjEffectName = String(params.vj_effect || "");
264
+ const effectIntensity = clamp(Number(params.effect_intensity || audio?.amplitude || 0.35), 0, 1);
265
+ const effectShader = getPostEffectShader(effectName);
266
+ const vjShader = getVJEffectShader(vjEffectName);
267
+ const selectedShader = vjShader || effectShader;
268
+ const selectedEffectName = vjShader ? `vj:${vjEffectName}` : `post:${effectName}`;
269
+ let program = this.compositeProgram;
270
+ if (selectedShader) {
271
+ try {
272
+ program = this.shaderManager.getProgram(
273
+ selectedEffectName,
274
+ FULLSCREEN_VERTEX_SHADER,
275
+ selectedShader
276
+ );
277
+ } catch (error) {
278
+ this.reportLayerError(layer, error, selectedEffectName);
279
+ program = this.compositeProgram;
280
+ }
281
+ }
282
+
283
+ this.setBlendMode(blend);
284
+
285
+ gl.useProgram(program);
286
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.fullscreenBuffer);
287
+ const positionLocation = gl.getAttribLocation(program, "a_position");
288
+ gl.enableVertexAttribArray(positionLocation);
289
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
290
+
291
+ gl.activeTexture(gl.TEXTURE0);
292
+ gl.bindTexture(gl.TEXTURE_2D, this.layerTexture);
293
+ this.setUniform1i(program, "u_texture", 0);
294
+ this.setUniform1f(program, "u_opacity", opacity);
295
+ this.setUniform1f(program, "u_time", time);
296
+ this.setUniform1f(program, "u_intensity", effectIntensity);
297
+ this.setUniform2f(program, "u_resolution", resolution[0], resolution[1]);
298
+
299
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
300
+ }
301
+
302
+ ensureLayerTarget(width, height) {
303
+ if (!this.layerTargetAvailable) {
304
+ return;
305
+ }
306
+
307
+ const [targetWidth, targetHeight] = this.resolveLayerTargetSize(width, height);
308
+ if (this.layerFramebuffer && this.layerTargetWidth === targetWidth && this.layerTargetHeight === targetHeight) {
309
+ return;
310
+ }
311
+ this.layerTargetWidth = targetWidth;
312
+ this.layerTargetHeight = targetHeight;
313
+
314
+ this.disposeLayerTarget();
315
+
316
+ const gl = this.gl;
317
+ this.layerFramebuffer = gl.createFramebuffer();
318
+ this.layerTexture = gl.createTexture();
319
+ this.layerDepthRenderbuffer = gl.createRenderbuffer();
320
+
321
+ gl.bindTexture(gl.TEXTURE_2D, this.layerTexture);
322
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, targetWidth, targetHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
323
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
324
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
325
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
326
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
327
+
328
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.layerFramebuffer);
329
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.layerTexture, 0);
330
+
331
+ gl.bindRenderbuffer(gl.RENDERBUFFER, this.layerDepthRenderbuffer);
332
+ gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, targetWidth, targetHeight);
333
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, this.layerDepthRenderbuffer);
334
+
335
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
336
+ if (status !== gl.FRAMEBUFFER_COMPLETE) {
337
+ console.warn("Layer framebuffer unavailable; falling back to direct rendering", status);
338
+ this.layerTargetAvailable = false;
339
+ this.disposeLayerTarget();
340
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
341
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
342
+ return;
343
+ }
344
+
345
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
346
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
347
+ }
348
+
349
+ disposeLayerTarget() {
350
+ if (this.layerTexture) {
351
+ this.gl.deleteTexture(this.layerTexture);
352
+ this.layerTexture = null;
353
+ }
354
+ if (this.layerDepthRenderbuffer) {
355
+ this.gl.deleteRenderbuffer(this.layerDepthRenderbuffer);
356
+ this.layerDepthRenderbuffer = null;
357
+ }
358
+ if (this.layerFramebuffer) {
359
+ this.gl.deleteFramebuffer(this.layerFramebuffer);
360
+ this.layerFramebuffer = null;
361
+ }
362
+ }
363
+
364
+ resolveLayerTargetSize(width, height) {
365
+ const gl = this.gl;
366
+ const maxTextureSize = Number(gl.getParameter(gl.MAX_TEXTURE_SIZE) || 4096);
367
+ let targetWidth = clamp(Math.floor(width), 1, maxTextureSize);
368
+ let targetHeight = clamp(Math.floor(height), 1, maxTextureSize);
369
+
370
+ const pixels = targetWidth * targetHeight;
371
+ if (pixels > MAX_LAYER_TARGET_PIXELS) {
372
+ const scale = Math.sqrt(MAX_LAYER_TARGET_PIXELS / pixels);
373
+ targetWidth = Math.max(1, Math.floor(targetWidth * scale));
374
+ targetHeight = Math.max(1, Math.floor(targetHeight * scale));
375
+ }
376
+
377
+ return [targetWidth, targetHeight];
378
+ }
379
+
380
+ setBlendMode(mode) {
381
+ if (mode === "add" || mode === "additive") {
382
+ this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
383
+ return;
384
+ }
385
+ this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
386
+ }
387
+
388
+ setUniform1f(program, uniformName, value) {
389
+ const location = this.gl.getUniformLocation(program, uniformName);
390
+ if (location === null) {
391
+ return;
392
+ }
393
+ this.gl.uniform1f(location, Number(value || 0));
394
+ }
395
+
396
+ setUniform2f(program, uniformName, x, y) {
397
+ const location = this.gl.getUniformLocation(program, uniformName);
398
+ if (location === null) {
399
+ return;
400
+ }
401
+ this.gl.uniform2f(location, Number(x || 0), Number(y || 0));
402
+ }
403
+
404
+ setUniform1i(program, uniformName, value) {
405
+ const location = this.gl.getUniformLocation(program, uniformName);
406
+ if (location === null) {
407
+ return;
408
+ }
409
+ this.gl.uniform1i(location, Number(value || 0));
410
+ }
411
+
412
+ reportLayerError(layer, error, phase) {
413
+ const name = String(layer?.name || "unnamed");
414
+ const shader = String(layer?.shader || layer?.type || "unknown");
415
+ const key = `${phase}:${name}:${shader}`;
416
+ if (this.layerErrorKeys.has(key)) {
417
+ return;
418
+ }
419
+ this.layerErrorKeys.add(key);
420
+ console.warn(`Layer render failed (${phase}) [${name}]`, error);
421
+ }
422
+ }
423
+
424
+ const isShaderLayer = (layer) => {
425
+ const type = String(layer?.type || "").toLowerCase();
426
+ return type === "shader" || !!layer?.shader || !!layer?.glsl;
427
+ };
428
+
429
+ const isParticleLayer = (layer) => {
430
+ const type = String(layer?.type || "").toLowerCase();
431
+ return type === "particle_field" || type === "particles" || type === "particle";
432
+ };
433
+
434
+ const isTextLayer = (layer) => {
435
+ const type = String(layer?.type || "").toLowerCase();
436
+ return type === "text" || type === "text_layer";
437
+ };
438
+
439
+ const defaultLayer = (audio) => ({
440
+ name: "wireframe_cube",
441
+ type: "geometry",
442
+ params: {
443
+ color_shift: Number(audio?.bands?.high || 0),
444
+ deform: audio?.fft || []
445
+ }
446
+ });
447
+
448
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
449
+
450
+ const hashString = (value) => {
451
+ let hash = 0;
452
+ for (let index = 0; index < value.length; index += 1) {
453
+ hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
454
+ }
455
+ return hash.toString(16);
456
+ };
@@ -0,0 +1,69 @@
1
+ export const FULLSCREEN_VERTEX_SHADER = `#version 300 es
2
+ in vec2 a_position;
3
+ out vec2 v_uv;
4
+ void main() {
5
+ v_uv = a_position * 0.5 + 0.5;
6
+ gl_Position = vec4(a_position, 0.0, 1.0);
7
+ }
8
+ `;
9
+
10
+ export class ShaderManager {
11
+ constructor(gl) {
12
+ this.gl = gl;
13
+ this.programCache = new Map();
14
+ }
15
+
16
+ getProgram(cacheKey, vertexSource, fragmentSource) {
17
+ const key = String(cacheKey);
18
+ const cached = this.programCache.get(key);
19
+ if (cached) {
20
+ return cached;
21
+ }
22
+
23
+ const program = this.createProgram(vertexSource, fragmentSource);
24
+ this.programCache.set(key, program);
25
+ return program;
26
+ }
27
+
28
+ createProgram(vertexSource, fragmentSource) {
29
+ const gl = this.gl;
30
+ const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
31
+ const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
32
+
33
+ const program = gl.createProgram();
34
+ gl.attachShader(program, vertexShader);
35
+ gl.attachShader(program, fragmentShader);
36
+ gl.linkProgram(program);
37
+
38
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
39
+ const info = gl.getProgramInfoLog(program);
40
+ gl.deleteShader(vertexShader);
41
+ gl.deleteShader(fragmentShader);
42
+ gl.deleteProgram(program);
43
+ throw new Error(`Program linking failed: ${info}`);
44
+ }
45
+
46
+ gl.deleteShader(vertexShader);
47
+ gl.deleteShader(fragmentShader);
48
+ return program;
49
+ }
50
+
51
+ dispose() {
52
+ for (const program of this.programCache.values()) {
53
+ this.gl.deleteProgram(program);
54
+ }
55
+ this.programCache.clear();
56
+ }
57
+ }
58
+
59
+ const compileShader = (gl, type, source) => {
60
+ const shader = gl.createShader(type);
61
+ gl.shaderSource(shader, source);
62
+ gl.compileShader(shader);
63
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
64
+ const info = gl.getShaderInfoLog(shader);
65
+ gl.deleteShader(shader);
66
+ throw new Error(`Shader compilation failed: ${info}`);
67
+ }
68
+ return shader;
69
+ };