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.
- checksums.yaml +4 -4
- data/README.md +70 -117
- data/docs/.nojekyll +0 -0
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -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 +225 -0
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -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 +494 -2
- data/frontend/src/audio-inspector.js +40 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/live-controls.js +131 -0
- data/frontend/src/main.js +1060 -16
- data/frontend/src/mapping-target-selector.js +109 -0
- 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 +157 -3
- data/frontend/src/renderer/layer-manager.js +442 -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/shape-editor-controls.js +157 -0
- data/frontend/src/visual-regression.js +67 -0
- data/frontend/src/visual-settings-preset.js +103 -0
- data/frontend/src/visuals/geometry.js +666 -0
- data/frontend/src/visuals/image-renderer.js +291 -0
- data/frontend/src/visuals/particle-system.js +56 -10
- data/frontend/src/visuals/shape-renderer.js +475 -0
- data/frontend/src/visuals/spectrogram-renderer.js +226 -0
- data/frontend/src/visuals/svg-arc.js +104 -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 +337 -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 +1280 -23
- data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
- 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 +275 -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 +132 -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 +938 -0
- data/lib/vizcore/renderer.rb +5 -0
- data/lib/vizcore/server/frame_broadcaster.rb +143 -8
- 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 +391 -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/shape.rb +719 -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 +28 -0
- data/scripts/browser_capture.mjs +75 -0
- data/sig/vizcore.rbs +461 -0
- metadata +94 -3
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
+
const numeric = coerceUniformNumber(value);
|
|
426
|
+
if (numeric === null) {
|
|
198
427
|
continue;
|
|
199
428
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
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
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
+
};
|