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