vizcore 0.1.0 → 1.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +70 -117
  3. data/docs/.nojekyll +0 -0
  4. data/docs/assets/playground-worker.js +373 -0
  5. data/docs/assets/playground.css +440 -0
  6. data/docs/assets/playground.js +652 -0
  7. data/docs/assets/site.css +744 -0
  8. data/docs/assets/vizcore-demo.gif +0 -0
  9. data/docs/assets/vizcore-poster.png +0 -0
  10. data/docs/assets/vj-tunnel.js +159 -0
  11. data/docs/index.html +225 -0
  12. data/docs/playground.html +81 -0
  13. data/docs/shape_dsl.md +269 -0
  14. data/examples/README.md +59 -0
  15. data/examples/assets/README.md +19 -0
  16. data/examples/audio_inspector.rb +34 -0
  17. data/examples/club_intro_drop.rb +78 -0
  18. data/examples/kansai_rubykaigi_visual.rb +70 -0
  19. data/examples/live_coding_minimal.rb +22 -0
  20. data/examples/midi_controller_show.rb +78 -0
  21. data/examples/midi_scene_switch.rb +3 -1
  22. data/examples/parser_visualizer.rb +48 -0
  23. data/examples/readme_demo.rb +17 -0
  24. data/examples/rhythm_geometry.rb +34 -0
  25. data/examples/ruby_crystal_show.rb +35 -0
  26. data/examples/shader_playground.rb +18 -0
  27. data/examples/unyo_liquid.rb +59 -0
  28. data/examples/vj_ambient_chill_room.rb +124 -0
  29. data/examples/vj_dnb_jungle.rb +170 -0
  30. data/examples/vj_festival_mainstage.rb +245 -0
  31. data/examples/vj_festival_mainstage.yml +17 -0
  32. data/examples/vj_glitch_industrial.rb +164 -0
  33. data/examples/vj_hiphop_cipher.rb +167 -0
  34. data/examples/vj_jpop_idol_live.rb +210 -0
  35. data/examples/vj_synthwave_retro.rb +173 -0
  36. data/examples/vj_techno_warehouse.rb +195 -0
  37. data/frontend/index.html +494 -2
  38. data/frontend/src/audio-inspector.js +40 -0
  39. data/frontend/src/custom-shape-param-controls.js +106 -0
  40. data/frontend/src/live-controls.js +131 -0
  41. data/frontend/src/main.js +1060 -16
  42. data/frontend/src/mapping-target-selector.js +109 -0
  43. data/frontend/src/midi-learn.js +194 -0
  44. data/frontend/src/performance-monitor.js +183 -0
  45. data/frontend/src/plugin-runtime.js +130 -0
  46. data/frontend/src/projector-mode.js +56 -0
  47. data/frontend/src/renderer/engine.js +157 -3
  48. data/frontend/src/renderer/layer-manager.js +442 -30
  49. data/frontend/src/renderer/shader-manager.js +26 -0
  50. data/frontend/src/runtime-control-preset.js +11 -0
  51. data/frontend/src/shader-error-overlay.js +29 -0
  52. data/frontend/src/shader-param-controls.js +93 -0
  53. data/frontend/src/shaders/builtins.js +380 -2
  54. data/frontend/src/shaders/post-effects.js +52 -0
  55. data/frontend/src/shape-editor-controls.js +157 -0
  56. data/frontend/src/visual-regression.js +67 -0
  57. data/frontend/src/visual-settings-preset.js +103 -0
  58. data/frontend/src/visuals/geometry.js +666 -0
  59. data/frontend/src/visuals/image-renderer.js +291 -0
  60. data/frontend/src/visuals/particle-system.js +56 -10
  61. data/frontend/src/visuals/shape-renderer.js +475 -0
  62. data/frontend/src/visuals/spectrogram-renderer.js +226 -0
  63. data/frontend/src/visuals/svg-arc.js +104 -0
  64. data/frontend/src/visuals/text-renderer.js +112 -11
  65. data/frontend/src/websocket-client.js +12 -1
  66. data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
  67. data/lib/vizcore/analysis/beat_detector.rb +4 -2
  68. data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
  69. data/lib/vizcore/analysis/feature_recorder.rb +159 -0
  70. data/lib/vizcore/analysis/feature_replay.rb +84 -0
  71. data/lib/vizcore/analysis/pipeline.rb +235 -11
  72. data/lib/vizcore/analysis/tap_tempo.rb +74 -0
  73. data/lib/vizcore/analysis.rb +4 -0
  74. data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
  75. data/lib/vizcore/audio/fixture_input.rb +65 -0
  76. data/lib/vizcore/audio/input_manager.rb +4 -2
  77. data/lib/vizcore/audio/mic_input.rb +24 -8
  78. data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
  79. data/lib/vizcore/audio.rb +1 -0
  80. data/lib/vizcore/cli/doctor.rb +159 -0
  81. data/lib/vizcore/cli/dsl_reference.rb +99 -0
  82. data/lib/vizcore/cli/layer_docs.rb +46 -0
  83. data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
  84. data/lib/vizcore/cli/scene_inspector.rb +136 -0
  85. data/lib/vizcore/cli/scene_validator.rb +337 -0
  86. data/lib/vizcore/cli/shader_template.rb +68 -0
  87. data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
  88. data/lib/vizcore/cli.rb +689 -18
  89. data/lib/vizcore/config.rb +103 -2
  90. data/lib/vizcore/control_preset.rb +68 -0
  91. data/lib/vizcore/dsl/engine.rb +277 -5
  92. data/lib/vizcore/dsl/layer_builder.rb +1280 -23
  93. data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
  94. data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
  95. data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
  96. data/lib/vizcore/dsl/reaction_builder.rb +44 -0
  97. data/lib/vizcore/dsl/scene_builder.rb +61 -5
  98. data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
  99. data/lib/vizcore/dsl/style_builder.rb +68 -0
  100. data/lib/vizcore/dsl/timeline_builder.rb +138 -0
  101. data/lib/vizcore/dsl/transition_controller.rb +77 -0
  102. data/lib/vizcore/dsl.rb +5 -1
  103. data/lib/vizcore/layer_catalog.rb +275 -0
  104. data/lib/vizcore/project_manifest.rb +152 -0
  105. data/lib/vizcore/renderer/png_writer.rb +57 -0
  106. data/lib/vizcore/renderer/render_sequence.rb +153 -0
  107. data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
  108. data/lib/vizcore/renderer/scene_serializer.rb +36 -3
  109. data/lib/vizcore/renderer/snapshot.rb +38 -0
  110. data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
  111. data/lib/vizcore/renderer.rb +5 -0
  112. data/lib/vizcore/server/frame_broadcaster.rb +143 -8
  113. data/lib/vizcore/server/gallery_app.rb +155 -0
  114. data/lib/vizcore/server/gallery_page.rb +100 -0
  115. data/lib/vizcore/server/gallery_runner.rb +48 -0
  116. data/lib/vizcore/server/rack_app.rb +203 -4
  117. data/lib/vizcore/server/runner.rb +391 -22
  118. data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
  119. data/lib/vizcore/server/websocket_handler.rb +60 -10
  120. data/lib/vizcore/server.rb +4 -0
  121. data/lib/vizcore/shape.rb +719 -0
  122. data/lib/vizcore/sync/osc_message.rb +103 -0
  123. data/lib/vizcore/sync/osc_receiver.rb +68 -0
  124. data/lib/vizcore/sync.rb +4 -0
  125. data/lib/vizcore/templates/midi_control_scene.rb +3 -1
  126. data/lib/vizcore/templates/plugin_layer.rb +20 -0
  127. data/lib/vizcore/templates/plugin_readme.md +23 -0
  128. data/lib/vizcore/templates/plugin_renderer.js +43 -0
  129. data/lib/vizcore/templates/plugin_scene.rb +14 -0
  130. data/lib/vizcore/templates/project_readme.md +7 -23
  131. data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
  132. data/lib/vizcore/version.rb +1 -1
  133. data/lib/vizcore.rb +28 -0
  134. data/scripts/browser_capture.mjs +75 -0
  135. data/sig/vizcore.rbs +461 -0
  136. metadata +94 -3
  137. data/docs/GETTING_STARTED.md +0 -105
@@ -1,7 +1,24 @@
1
1
  import { getBuiltinShader } from "../shaders/builtins.js";
2
2
  import { getPostEffectShader } from "../shaders/post-effects.js";
3
- import { buildWireframeLines, estimateDeformFromSpectrum } from "../visuals/geometry.js";
3
+ import { SHADER_ERROR_EVENT, buildShaderErrorDetail } from "../shader-error-overlay.js";
4
+ import {
5
+ buildPresetMeshLines,
6
+ buildRadialBlobLines,
7
+ buildShapeLines,
8
+ buildWaveformLines,
9
+ buildWireframeLines,
10
+ estimateDeformFromSpectrum
11
+ } from "../visuals/geometry.js";
12
+ import { ImageRenderer } from "../visuals/image-renderer.js";
4
13
  import { ParticleSystem } from "../visuals/particle-system.js";
14
+ import {
15
+ normalizePluginLineOutput,
16
+ normalizePluginShaderOutput,
17
+ resolveLayerRenderer,
18
+ resolveShaderRenderer
19
+ } from "../plugin-runtime.js";
20
+ import { SpectrogramRenderer } from "../visuals/spectrogram-renderer.js";
21
+ import { ShapeRenderer } from "../visuals/shape-renderer.js";
5
22
  import { TextRenderer } from "../visuals/text-renderer.js";
6
23
  import { getVJEffectShader } from "../visuals/vj-effects.js";
7
24
  import { FULLSCREEN_VERTEX_SHADER } from "./shader-manager.js";
@@ -42,6 +59,117 @@ const FULLSCREEN_VERTICES = new Float32Array([
42
59
  ]);
43
60
  const MAX_LAYER_TARGET_PIXELS = 4_194_304;
44
61
 
62
+ export const coerceUniformNumber = (value) => {
63
+ if (typeof value === "boolean") {
64
+ return value ? 1 : 0;
65
+ }
66
+
67
+ if (typeof value !== "number" && typeof value !== "string") {
68
+ return null;
69
+ }
70
+
71
+ if (typeof value === "string" && value.trim() === "") {
72
+ return null;
73
+ }
74
+
75
+ const numeric = Number(value);
76
+ if (!Number.isFinite(numeric)) {
77
+ return null;
78
+ }
79
+
80
+ return numeric;
81
+ };
82
+
83
+ export const shaderParamUniformNames = (rawKey) => {
84
+ const safeKey = String(rawKey || "").replace(/[^a-zA-Z0-9_]/g, "_");
85
+ if (!safeKey) {
86
+ return [];
87
+ }
88
+
89
+ const names = [`u_param_${safeKey}`];
90
+
91
+ if (safeKey.startsWith("param_")) {
92
+ names.push(`u_${safeKey}`);
93
+ }
94
+
95
+ return [...new Set(names)];
96
+ };
97
+
98
+ export const shaderGlobalUniformNames = (rawKey) => {
99
+ const safeKey = String(rawKey || "").replace(/[^a-zA-Z0-9_]/g, "_");
100
+ if (!safeKey) {
101
+ return [];
102
+ }
103
+
104
+ const names = safeKey.startsWith("global_")
105
+ ? [`u_${safeKey}`, `u_global_${safeKey.slice(7)}`]
106
+ : [`u_global_${safeKey}`];
107
+
108
+ return [...new Set(names)];
109
+ };
110
+
111
+ export const normalizeSpectrum = (value, size = 32) => {
112
+ const input = Array.isArray(value) || ArrayBuffer.isView(value) ? Array.from(value) : [];
113
+ const output = new Float32Array(size);
114
+
115
+ for (let index = 0; index < size; index += 1) {
116
+ const numeric = Number(input[index] || 0);
117
+ output[index] = Number.isFinite(numeric) ? Math.min(Math.max(numeric, 0), 1) : 0;
118
+ }
119
+
120
+ return output;
121
+ };
122
+
123
+ export const normalizeBlendMode = (mode) => {
124
+ const value = String(mode || "alpha").toLowerCase();
125
+ if (value === "normal" || value === "alpha") return "alpha";
126
+ if (value === "add" || value === "additive") return "add";
127
+ if (value === "multiply") return "multiply";
128
+ if (value === "screen") return "screen";
129
+ if (value === "difference") return "difference";
130
+ return "alpha";
131
+ };
132
+
133
+ export const normalizePaletteColors = (value) => {
134
+ const input = Array.isArray(value) ? value : [];
135
+ return input
136
+ .map((entry) => String(entry || "").trim())
137
+ .filter((entry) => entry.length > 0);
138
+ };
139
+
140
+ export const parseHexColor = (value) => {
141
+ const raw = String(value || "").trim();
142
+ const match = raw.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
143
+ if (!match) {
144
+ return null;
145
+ }
146
+
147
+ const hex = match[1].length === 3
148
+ ? match[1].split("").map((char) => `${char}${char}`).join("")
149
+ : match[1];
150
+
151
+ return [0, 2, 4].map((offset) => parseInt(hex.slice(offset, offset + 2), 16) / 255);
152
+ };
153
+
154
+ export const resolveLayerCssColor = (params = {}, fallback = "#e5f3ff", paletteIndex = 0) => {
155
+ const explicitColor = String(params?.color || "").trim();
156
+ if (explicitColor) {
157
+ return explicitColor;
158
+ }
159
+
160
+ const palette = normalizePaletteColors(params?.palette);
161
+ if (palette.length === 0) {
162
+ return fallback;
163
+ }
164
+
165
+ return palette[Math.abs(Number(paletteIndex) || 0) % palette.length];
166
+ };
167
+
168
+ export const resolveLayerRgbColor = (params = {}, fallback = null, paletteIndex = 0) => {
169
+ const parsed = parseHexColor(resolveLayerCssColor(params, "", paletteIndex));
170
+ return parsed || fallback;
171
+ };
172
+
45
173
  export class LayerManager {
46
174
  constructor(gl, shaderManager) {
47
175
  this.gl = gl;
@@ -77,39 +205,42 @@ export class LayerManager {
77
205
 
78
206
  this.particleSystem = new ParticleSystem(this.gl, this.shaderManager);
79
207
  this.textRenderer = new TextRenderer(this.gl, this.shaderManager);
208
+ this.imageRenderer = new ImageRenderer(this.gl, this.shaderManager);
209
+ this.shapeRenderer = new ShapeRenderer(this.gl, this.shaderManager);
210
+ this.spectrogramRenderer = new SpectrogramRenderer(this.gl, this.shaderManager);
80
211
 
81
212
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.fullscreenBuffer);
82
213
  this.gl.bufferData(this.gl.ARRAY_BUFFER, FULLSCREEN_VERTICES, this.gl.STATIC_DRAW);
83
214
  }
84
215
 
85
- renderScene({ layers, audio, time, rotation, resolution }) {
216
+ renderScene({ layers, audio, time, rotation, resolution, globals, visualSettings }) {
86
217
  const layerList = Array.isArray(layers) && layers.length > 0 ? layers : [defaultLayer(audio)];
87
218
  const width = Math.max(1, Math.floor(Number(resolution?.[0] || 1)));
88
219
  const height = Math.max(1, Math.floor(Number(resolution?.[1] || 1)));
89
220
  this.ensureLayerTarget(width, height);
90
221
 
91
222
  if (!this.layerTargetAvailable || !this.layerFramebuffer || !this.layerTexture) {
92
- for (const layer of layerList) {
223
+ layerList.forEach((layer, index) => {
93
224
  try {
94
225
  const blend = String(layer?.params?.blend || "alpha").toLowerCase();
95
226
  this.setBlendMode(blend);
96
- this.renderLayer(layer, audio, time, rotation, [width, height]);
227
+ this.renderLayer(layer, audio, time, rotation, [width, height], globals, visualSettings, index);
97
228
  } catch (error) {
98
229
  this.reportLayerError(layer, error, "direct-render");
99
230
  }
100
- }
231
+ });
101
232
  this.setBlendMode("alpha");
102
233
  return;
103
234
  }
104
235
 
105
- for (const layer of layerList) {
236
+ layerList.forEach((layer, index) => {
106
237
  try {
107
238
  this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.layerFramebuffer);
108
239
  this.gl.viewport(0, 0, this.layerTargetWidth, this.layerTargetHeight);
109
240
  this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
110
241
  this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
111
242
 
112
- this.renderLayer(layer, audio, time, rotation, [this.layerTargetWidth, this.layerTargetHeight]);
243
+ this.renderLayer(layer, audio, time, rotation, [this.layerTargetWidth, this.layerTargetHeight], globals, visualSettings, index);
113
244
 
114
245
  this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
115
246
  this.gl.viewport(0, 0, width, height);
@@ -119,27 +250,93 @@ export class LayerManager {
119
250
  this.gl.viewport(0, 0, width, height);
120
251
  this.reportLayerError(layer, error, "layer-pass");
121
252
  }
122
- }
253
+ });
123
254
  this.setBlendMode("alpha");
124
255
  }
125
256
 
126
- renderLayer(layer, audio, time, rotation, resolution) {
257
+ renderLayer(layer, audio, time, rotation, resolution, globals, visualSettings, paletteIndex = 0) {
127
258
  if (isParticleLayer(layer)) {
128
- this.renderParticleLayer(layer, audio, time);
259
+ this.renderParticleLayer(layer, audio, time, paletteIndex);
129
260
  return;
130
261
  }
131
262
  if (isTextLayer(layer)) {
132
- this.renderTextLayer(layer, audio, time);
263
+ this.renderTextLayer(layer, audio, time, paletteIndex);
264
+ return;
265
+ }
266
+ if (isImageLayer(layer) || isVideoLayer(layer)) {
267
+ this.renderImageLayer(layer, audio);
268
+ return;
269
+ }
270
+ if (isWaveformLayer(layer)) {
271
+ this.renderWaveformLayer(layer, audio, time, paletteIndex);
272
+ return;
273
+ }
274
+ if (isSpectrogramLayer(layer)) {
275
+ this.renderSpectrogramLayer(layer, audio);
276
+ return;
277
+ }
278
+ if (isShapeLayer(layer)) {
279
+ this.renderShapeLayer(layer, audio, time, resolution, paletteIndex);
133
280
  return;
134
281
  }
135
282
  if (isShaderLayer(layer)) {
136
- this.renderShaderLayer(layer, audio, time, resolution);
283
+ this.renderShaderLayer(layer, audio, time, resolution, globals, visualSettings);
284
+ return;
285
+ }
286
+ if (this.renderPluginLayer(layer, audio, time, rotation, resolution, globals, visualSettings, paletteIndex)) {
137
287
  return;
138
288
  }
139
- this.renderGeometryLayer(layer, audio, rotation);
289
+ this.renderGeometryLayer(layer, audio, rotation, time, paletteIndex);
290
+ }
291
+
292
+ renderPluginLayer(layer, audio, time, rotation, resolution, globals, visualSettings, paletteIndex = 0) {
293
+ const context = {
294
+ layer,
295
+ audio,
296
+ time,
297
+ rotation,
298
+ resolution,
299
+ globals,
300
+ visualSettings,
301
+ paletteIndex
302
+ };
303
+
304
+ const renderer = resolveLayerRenderer(layer?.type);
305
+ if (renderer && this.renderPluginOutput(layer, renderer(context), audio, time, resolution, globals, visualSettings, paletteIndex)) {
306
+ return true;
307
+ }
308
+
309
+ const shaderRenderer = resolveShaderRenderer(layer?.type);
310
+ if (shaderRenderer && this.renderPluginOutput(layer, shaderRenderer(context), audio, time, resolution, globals, visualSettings, paletteIndex)) {
311
+ return true;
312
+ }
313
+
314
+ return false;
315
+ }
316
+
317
+ renderPluginOutput(layer, output, audio, time, resolution, globals, visualSettings, paletteIndex = 0) {
318
+ const lines = normalizePluginLineOutput(output);
319
+ if (lines) {
320
+ const fallbackColor = resolveLayerRgbColor(layer?.params || {}, [0.82, 0.92, 1.0], paletteIndex);
321
+ this.renderLinePoints(lines.points, lines.color || fallbackColor);
322
+ return true;
323
+ }
324
+
325
+ const shader = normalizePluginShaderOutput(output);
326
+ if (!shader) {
327
+ return false;
328
+ }
329
+
330
+ this.renderShaderLayer({
331
+ ...layer,
332
+ shader: layer?.shader || "default",
333
+ glsl: `plugin:${String(layer?.type || "layer")}:${shader.cacheKey}`,
334
+ glsl_source: shader.fragmentShader
335
+ }, audio, time, resolution, globals, visualSettings);
336
+ return true;
140
337
  }
141
338
 
142
- renderShaderLayer(layer, audio, time, resolution) {
339
+ renderShaderLayer(layer, audio, time, resolution, globals, visualSettings) {
143
340
  const shaderName = String(layer?.shader || "gradient_pulse");
144
341
  const customSource = typeof layer?.glsl_source === "string" ? layer.glsl_source : null;
145
342
  const fragmentShader = customSource || getBuiltinShader(shaderName);
@@ -151,6 +348,7 @@ export class LayerManager {
151
348
  program = this.shaderManager.getProgram(cacheKey, FULLSCREEN_VERTEX_SHADER, fragmentShader);
152
349
  } catch (error) {
153
350
  if (customSource) {
351
+ this.reportShaderError(layer, error, "custom-shader");
154
352
  console.warn("Failed to compile custom GLSL, falling back to builtin shader", error);
155
353
  try {
156
354
  program = this.shaderManager.getProgram(
@@ -159,9 +357,11 @@ export class LayerManager {
159
357
  getBuiltinShader(shaderName)
160
358
  );
161
359
  } catch (builtinError) {
360
+ this.reportShaderError(layer, builtinError, "builtin-shader-fallback");
162
361
  this.reportLayerError(layer, builtinError, "builtin-shader-fallback");
163
362
  }
164
363
  } else {
364
+ this.reportShaderError(layer, error, "builtin-shader");
165
365
  this.reportLayerError(layer, error, "builtin-shader");
166
366
  }
167
367
 
@@ -183,6 +383,8 @@ export class LayerManager {
183
383
  gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
184
384
 
185
385
  const bands = audio?.bands || {};
386
+ const onsets = audio?.onsets || {};
387
+ const drums = audio?.drums || {};
186
388
  this.setUniform1f(program, "u_time", time);
187
389
  this.setUniform2f(program, "u_resolution", resolution[0], resolution[1]);
188
390
  this.setUniform1f(program, "u_amplitude", audio?.amplitude || 0);
@@ -190,31 +392,71 @@ export class LayerManager {
190
392
  this.setUniform1f(program, "u_mid", bands.mid || 0);
191
393
  this.setUniform1f(program, "u_high", bands.high || 0);
192
394
  this.setUniform1f(program, "u_beat", audio?.beat ? 1 : 0);
395
+ this.setUniform1f(program, "u_beat_pulse", audio?.beat_pulse || (audio?.beat ? 1 : 0));
396
+ this.setUniform1f(program, "u_onset", audio?.onset || 0);
397
+ this.setUniform1f(program, "u_sub_onset", onsets.sub || 0);
398
+ this.setUniform1f(program, "u_low_onset", onsets.low || 0);
399
+ this.setUniform1f(program, "u_mid_onset", onsets.mid || 0);
400
+ this.setUniform1f(program, "u_high_onset", onsets.high || 0);
401
+ this.setUniform1f(program, "u_kick", drums.kick || 0);
402
+ this.setUniform1f(program, "u_snare", drums.snare || 0);
403
+ this.setUniform1f(program, "u_hihat", drums.hihat || 0);
193
404
  this.setUniform1f(program, "u_bpm", audio?.bpm || 0);
405
+ const spectrum = normalizeSpectrum(audio?.fft, 32);
406
+ this.setUniform1fv(program, "u_fft[0]", spectrum);
407
+ this.setUniform1f(program, "u_fft_size", spectrum.length);
408
+ this.setUniform1f(program, "u_visual_gain", audio?.visual_gain || visualSettings?.visualGain || 1);
409
+ this.setUniform1f(program, "u_bass_boost", audio?.bass_boost || visualSettings?.bassBoost || 1);
410
+ this.setUniform1f(program, "u_wobble_amount", audio?.wobble_amount || visualSettings?.wobbleAmount || 1);
411
+
412
+ const runtimeGlobals = globals && typeof globals === "object" ? globals : {};
413
+ for (const [key, value] of Object.entries(runtimeGlobals)) {
414
+ const numeric = coerceUniformNumber(value);
415
+ if (numeric === null) {
416
+ continue;
417
+ }
418
+ for (const uniformName of shaderGlobalUniformNames(key)) {
419
+ this.setUniform1f(program, uniformName, numeric);
420
+ }
421
+ }
194
422
 
195
423
  const params = layer?.params || {};
196
424
  for (const [key, value] of Object.entries(params)) {
197
- if (typeof value !== "number") {
425
+ const numeric = coerceUniformNumber(value);
426
+ if (numeric === null) {
198
427
  continue;
199
428
  }
200
- const safeKey = key.replace(/[^a-zA-Z0-9_]/g, "_");
201
- this.setUniform1f(program, `u_param_${safeKey}`, value);
429
+ for (const uniformName of shaderParamUniformNames(key)) {
430
+ this.setUniform1f(program, uniformName, numeric);
431
+ }
202
432
  }
203
433
 
204
434
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
205
435
  }
206
436
 
207
- renderGeometryLayer(layer, audio, rotation) {
437
+ renderGeometryLayer(layer, audio, rotation, time, paletteIndex = 0) {
208
438
  const gl = this.gl;
209
439
  const params = layer?.params || {};
210
440
  const colorShift = clamp(Number(params.color_shift || 0), 0, 1);
211
441
  const deform = estimateDeformFromSpectrum(params.deform ?? audio?.fft);
212
- const points = buildWireframeLines({
442
+ const type = String(layer?.type || "").toLowerCase();
443
+ let points = buildWireframeLines({
213
444
  rotationY: rotation,
214
445
  rotationX: rotation * 0.8,
215
446
  deform
216
447
  });
217
448
 
449
+ if (type === "radial_blob") {
450
+ points = buildRadialBlobLines({ time, params, audio });
451
+ } else if (isMeshLayer(layer)) {
452
+ points = buildPresetMeshLines({
453
+ rotationY: rotation,
454
+ rotationX: rotation * 0.8,
455
+ deform,
456
+ params
457
+ });
458
+ }
459
+
218
460
  gl.useProgram(this.geometryProgram);
219
461
  gl.bindBuffer(gl.ARRAY_BUFFER, this.geometryBuffer);
220
462
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.DYNAMIC_DRAW);
@@ -222,38 +464,137 @@ export class LayerManager {
222
464
  gl.vertexAttribPointer(this.geometryPositionLocation, 2, gl.FLOAT, false, 0, 0);
223
465
 
224
466
  const amplitude = clamp(Number(audio?.amplitude || 0), 0, 1);
225
- gl.uniform3f(
226
- this.geometryColorLocation,
227
- 0.45 + amplitude * 0.45,
467
+ const pulse = clamp(Number(audio?.beat_pulse || 0), 0, 1);
468
+ const fallbackColor = [
469
+ 0.45 + amplitude * 0.45 + pulse * 0.15,
228
470
  0.75 + colorShift * 0.2,
229
471
  0.96
230
- );
472
+ ];
473
+ const color = resolveLayerRgbColor(params, fallbackColor, paletteIndex);
474
+ gl.uniform3f(this.geometryColorLocation, color[0], color[1], color[2]);
231
475
  gl.drawArrays(gl.LINES, 0, points.length / 2);
232
476
  }
233
477
 
234
- renderParticleLayer(layer, audio, time) {
478
+ renderWaveformLayer(layer, audio, time, paletteIndex = 0) {
479
+ const gl = this.gl;
480
+ const params = layer?.params || {};
481
+ const points = buildWaveformLines({ time, params, audio });
482
+
483
+ if (points.length === 0) {
484
+ return;
485
+ }
486
+
487
+ gl.useProgram(this.geometryProgram);
488
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.geometryBuffer);
489
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.DYNAMIC_DRAW);
490
+ gl.enableVertexAttribArray(this.geometryPositionLocation);
491
+ gl.vertexAttribPointer(this.geometryPositionLocation, 2, gl.FLOAT, false, 0, 0);
492
+
493
+ const amplitude = clamp(Number(audio?.amplitude || 0), 0, 1);
494
+ const high = clamp(Number(audio?.bands?.high || 0), 0, 1);
495
+ const fallbackColor = [
496
+ 0.28 + high * 0.32,
497
+ 0.86 + amplitude * 0.14,
498
+ 0.72 + high * 0.22
499
+ ];
500
+ const color = resolveLayerRgbColor(params, fallbackColor, paletteIndex);
501
+ gl.uniform3f(this.geometryColorLocation, color[0], color[1], color[2]);
502
+ gl.drawArrays(gl.LINES, 0, points.length / 2);
503
+ }
504
+
505
+ renderParticleLayer(layer, audio, time, paletteIndex = 0) {
235
506
  const params = layer?.params || {};
236
507
  this.particleSystem.render({
237
508
  count: Number(params.count || 2400),
238
509
  speed: Number(params.speed || audio?.amplitude || 0),
239
510
  size: Number(params.size || 2.0),
511
+ forceField: String(params.force_field || "drift"),
512
+ turbulence: Number(params.turbulence || 0),
513
+ bassExplosion: Number(params.bass_explosion || 0),
514
+ sparkle: Number(params.sparkle || 0),
515
+ color: resolveLayerRgbColor(params, null, paletteIndex),
240
516
  audio,
241
517
  time
242
518
  });
243
519
  }
244
520
 
245
- renderTextLayer(layer, audio, time) {
521
+ renderTextLayer(layer, audio, time, paletteIndex = 0) {
246
522
  const params = layer?.params || {};
247
523
  this.textRenderer.render({
248
524
  content: params.content || "VIZCORE",
249
525
  fontSize: Number(params.font_size || 120),
250
- color: params.color || "#e5f3ff",
526
+ color: resolveLayerCssColor(params, "#e5f3ff", paletteIndex),
527
+ fontFamily: params.font || params.font_family,
528
+ align: params.align,
529
+ letterSpacing: params.letter_spacing,
530
+ strokeWidth: params.stroke_width,
531
+ strokeColor: params.stroke_color,
532
+ shadowColor: params.shadow_color,
533
+ shadowBlur: params.shadow_blur,
251
534
  glowStrength: Number(params.glow_strength ?? 0.15),
252
535
  audio,
253
536
  time
254
537
  });
255
538
  }
256
539
 
540
+ renderImageLayer(layer, audio) {
541
+ const params = layer?.params || {};
542
+ this.imageRenderer.render({
543
+ src: params.src || params.file,
544
+ fit: params.fit,
545
+ scale: params.scale,
546
+ rotation: params.rotation,
547
+ playbackRate: params.playback_rate,
548
+ invert: params.invert,
549
+ audio
550
+ });
551
+ }
552
+
553
+ renderSpectrogramLayer(layer, audio) {
554
+ this.spectrogramRenderer.render({
555
+ key: layer?.name || "spectrogram",
556
+ audio,
557
+ params: layer?.params || {}
558
+ });
559
+ }
560
+
561
+ renderShapeLayer(layer, audio, time, resolution, paletteIndex = 0) {
562
+ const params = layer?.params || {};
563
+ const amplitude = clamp(Number(audio?.amplitude || 0), 0, 1);
564
+ const fallbackColor = [0.85, 0.50 + amplitude * 0.24, 0.95];
565
+ const cssColor = resolveLayerCssColor(params, "#d98cff", paletteIndex);
566
+ const rendered = this.shapeRenderer.render({
567
+ params,
568
+ color: cssColor,
569
+ resolution,
570
+ audio,
571
+ time
572
+ });
573
+ if (rendered) {
574
+ return;
575
+ }
576
+
577
+ const points = buildShapeLines({ params });
578
+
579
+ if (points.length === 0) {
580
+ return;
581
+ }
582
+
583
+ const color = resolveLayerRgbColor(params, fallbackColor, paletteIndex);
584
+ this.renderLinePoints(points, color);
585
+ }
586
+
587
+ renderLinePoints(points, color) {
588
+ const gl = this.gl;
589
+ gl.useProgram(this.geometryProgram);
590
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.geometryBuffer);
591
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.DYNAMIC_DRAW);
592
+ gl.enableVertexAttribArray(this.geometryPositionLocation);
593
+ gl.vertexAttribPointer(this.geometryPositionLocation, 2, gl.FLOAT, false, 0, 0);
594
+ gl.uniform3f(this.geometryColorLocation, color[0], color[1], color[2]);
595
+ gl.drawArrays(gl.LINES, 0, points.length / 2);
596
+ }
597
+
257
598
  compositeLayer(layer, { audio, time, resolution }) {
258
599
  const gl = this.gl;
259
600
  const params = layer?.params || {};
@@ -378,11 +719,25 @@ export class LayerManager {
378
719
  }
379
720
 
380
721
  setBlendMode(mode) {
381
- if (mode === "add" || mode === "additive") {
382
- this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
383
- return;
722
+ const blendMode = normalizeBlendMode(mode);
723
+ this.gl.blendEquation(this.gl.FUNC_ADD);
724
+
725
+ switch (blendMode) {
726
+ case "add":
727
+ this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE);
728
+ return;
729
+ case "multiply":
730
+ this.gl.blendFunc(this.gl.DST_COLOR, this.gl.ONE_MINUS_SRC_ALPHA);
731
+ return;
732
+ case "screen":
733
+ this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_COLOR);
734
+ return;
735
+ case "difference":
736
+ this.gl.blendFunc(this.gl.ONE_MINUS_DST_COLOR, this.gl.ONE_MINUS_SRC_COLOR);
737
+ return;
738
+ default:
739
+ this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
384
740
  }
385
- this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
386
741
  }
387
742
 
388
743
  setUniform1f(program, uniformName, value) {
@@ -393,6 +748,14 @@ export class LayerManager {
393
748
  this.gl.uniform1f(location, Number(value || 0));
394
749
  }
395
750
 
751
+ setUniform1fv(program, uniformName, values) {
752
+ const location = this.gl.getUniformLocation(program, uniformName);
753
+ if (location === null) {
754
+ return;
755
+ }
756
+ this.gl.uniform1fv(location, values);
757
+ }
758
+
396
759
  setUniform2f(program, uniformName, x, y) {
397
760
  const location = this.gl.getUniformLocation(program, uniformName);
398
761
  if (location === null) {
@@ -419,6 +782,18 @@ export class LayerManager {
419
782
  this.layerErrorKeys.add(key);
420
783
  console.warn(`Layer render failed (${phase}) [${name}]`, error);
421
784
  }
785
+
786
+ reportShaderError(layer, error, phase) {
787
+ const detail = buildShaderErrorDetail({ layer, error, phase });
788
+ const key = `shader:${detail.phase}:${detail.name}:${detail.shader}:${detail.message}`;
789
+ if (this.layerErrorKeys.has(key)) {
790
+ return;
791
+ }
792
+ this.layerErrorKeys.add(key);
793
+ if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") {
794
+ window.dispatchEvent(new CustomEvent(SHADER_ERROR_EVENT, { detail }));
795
+ }
796
+ }
422
797
  }
423
798
 
424
799
  const isShaderLayer = (layer) => {
@@ -436,6 +811,43 @@ const isTextLayer = (layer) => {
436
811
  return type === "text" || type === "text_layer";
437
812
  };
438
813
 
814
+ const isSvgLayer = (layer) => {
815
+ const type = String(layer?.type || "").toLowerCase();
816
+ return type === "svg" || type === "svg_layer";
817
+ };
818
+
819
+ const isRasterImageLayer = (layer) => {
820
+ const type = String(layer?.type || "").toLowerCase();
821
+ return type === "image" || type === "image_layer" || type === "photo";
822
+ };
823
+
824
+ const isImageLayer = (layer) => isSvgLayer(layer) || isRasterImageLayer(layer);
825
+
826
+ const isVideoLayer = (layer) => {
827
+ const type = String(layer?.type || "").toLowerCase();
828
+ return type === "video" || type === "video_layer" || type === "footage";
829
+ };
830
+
831
+ const isWaveformLayer = (layer) => {
832
+ const type = String(layer?.type || "").toLowerCase();
833
+ return type === "waveform" || type === "waveform_layer";
834
+ };
835
+
836
+ const isSpectrogramLayer = (layer) => {
837
+ const type = String(layer?.type || "").toLowerCase();
838
+ return type === "spectrogram" || type === "spectrogram_layer";
839
+ };
840
+
841
+ const isShapeLayer = (layer) => {
842
+ const type = String(layer?.type || "").toLowerCase();
843
+ return type === "shape" || type === "shapes" || type === "shape_layer";
844
+ };
845
+
846
+ const isMeshLayer = (layer) => {
847
+ const type = String(layer?.type || "").toLowerCase();
848
+ return type === "mesh" || type === "mesh_layer" || type === "preset_mesh";
849
+ };
850
+
439
851
  const defaultLayer = (audio) => ({
440
852
  name: "wireframe_cube",
441
853
  type: "geometry",
@@ -6,6 +6,7 @@ void main() {
6
6
  gl_Position = vec4(a_position, 0.0, 1.0);
7
7
  }
8
8
  `;
9
+ export const SHADER_COMPILE_EVENT = "vizcore:shader-compile";
9
10
 
10
11
  export class ShaderManager {
11
12
  constructor(gl) {
@@ -20,8 +21,13 @@ export class ShaderManager {
20
21
  return cached;
21
22
  }
22
23
 
24
+ const startedAtMs = nowMs();
23
25
  const program = this.createProgram(vertexSource, fragmentSource);
24
26
  this.programCache.set(key, program);
27
+ dispatchShaderCompileEvent({
28
+ cacheKey: key,
29
+ compileMs: nowMs() - startedAtMs,
30
+ });
25
31
  return program;
26
32
  }
27
33
 
@@ -67,3 +73,23 @@ const compileShader = (gl, type, source) => {
67
73
  }
68
74
  return shader;
69
75
  };
76
+
77
+ const nowMs = () => {
78
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
79
+ return performance.now();
80
+ }
81
+
82
+ return Date.now();
83
+ };
84
+
85
+ const dispatchShaderCompileEvent = (detail) => {
86
+ if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") {
87
+ return;
88
+ }
89
+
90
+ if (typeof CustomEvent !== "function") {
91
+ return;
92
+ }
93
+
94
+ window.dispatchEvent(new CustomEvent(SHADER_COMPILE_EVENT, { detail }));
95
+ };