vizcore 1.1.0 → 1.2.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/frontend/index.html +24 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +447 -57
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +391 -10
- data/frontend/src/renderer/layer-manager.js +472 -71
- data/frontend/src/runtime-control-preset.js +44 -0
- data/frontend/src/scene-patches.js +159 -0
- data/frontend/src/shader-error-overlay.js +1 -0
- data/frontend/src/visuals/image-renderer.js +19 -0
- data/frontend/src/visuals/particle-system.js +10 -0
- data/frontend/src/visuals/shape-renderer.js +13 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -0
- data/frontend/src/visuals/text-renderer.js +13 -0
- data/frontend/src/websocket-client.js +6 -0
- data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
- data/lib/vizcore/analysis/feature_recorder.rb +117 -7
- data/lib/vizcore/analysis/feature_replay.rb +48 -9
- data/lib/vizcore/analysis/pipeline.rb +258 -9
- data/lib/vizcore/analysis/tap_tempo.rb +17 -2
- data/lib/vizcore/audio/calibration.rb +156 -0
- data/lib/vizcore/audio/file_input.rb +28 -0
- data/lib/vizcore/audio/input_manager.rb +36 -1
- data/lib/vizcore/audio/midi_input.rb +5 -0
- data/lib/vizcore/audio/ring_buffer.rb +22 -0
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/dsl_reference.rb +64 -8
- data/lib/vizcore/cli/plugin_checker.rb +93 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
- data/lib/vizcore/cli/scene_inspector.rb +35 -1
- data/lib/vizcore/cli/scene_validator.rb +487 -39
- data/lib/vizcore/cli/shader_template.rb +7 -2
- data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
- data/lib/vizcore/cli.rb +268 -15
- data/lib/vizcore/config.rb +40 -3
- data/lib/vizcore/control_preset.rb +29 -0
- data/lib/vizcore/deep_copy.rb +21 -0
- data/lib/vizcore/dsl/color_helpers.rb +155 -0
- data/lib/vizcore/dsl/engine.rb +219 -23
- data/lib/vizcore/dsl/layer_builder.rb +278 -15
- data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
- data/lib/vizcore/dsl/layout_helpers.rb +290 -0
- data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
- data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
- data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
- data/lib/vizcore/dsl/reaction_builder.rb +1 -0
- data/lib/vizcore/dsl/scene_builder.rb +83 -13
- data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
- data/lib/vizcore/dsl/style_builder.rb +3 -0
- data/lib/vizcore/dsl/timeline_builder.rb +91 -8
- data/lib/vizcore/dsl/transition_controller.rb +157 -18
- data/lib/vizcore/dsl.rb +2 -0
- data/lib/vizcore/layer_catalog.rb +1 -0
- data/lib/vizcore/plugin_asset_policy.rb +55 -0
- data/lib/vizcore/project_manifest.rb +12 -2
- data/lib/vizcore/renderer/render_sequence.rb +104 -13
- data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +469 -23
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +676 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +39 -16
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +33 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +55 -4
- metadata +18 -3
|
@@ -58,6 +58,8 @@ const FULLSCREEN_VERTICES = new Float32Array([
|
|
|
58
58
|
1.0, 1.0
|
|
59
59
|
]);
|
|
60
60
|
const MAX_LAYER_TARGET_PIXELS = 4_194_304;
|
|
61
|
+
const MIN_LAYER_RESOLUTION_SCALE = 0.1;
|
|
62
|
+
const SHADER_CACHE_VERSION = "v2";
|
|
61
63
|
|
|
62
64
|
export const coerceUniformNumber = (value) => {
|
|
63
65
|
if (typeof value === "boolean") {
|
|
@@ -130,6 +132,36 @@ export const normalizeBlendMode = (mode) => {
|
|
|
130
132
|
return "alpha";
|
|
131
133
|
};
|
|
132
134
|
|
|
135
|
+
export const resolveLayerResolutionScale = (params = {}, visualSettings = {}) => {
|
|
136
|
+
const rawValue = params?.resolution_scale
|
|
137
|
+
?? params?.resolutionScale
|
|
138
|
+
?? params?.target_resolution_scale
|
|
139
|
+
?? params?.targetResolutionScale
|
|
140
|
+
?? 1;
|
|
141
|
+
const layerScale = clamp(Number(rawValue), MIN_LAYER_RESOLUTION_SCALE, 1);
|
|
142
|
+
const safeScale = visualSettings?.safeModeActive
|
|
143
|
+
? clamp(Number(visualSettings?.safeModeScale || 0.75), MIN_LAYER_RESOLUTION_SCALE, 1)
|
|
144
|
+
: 1;
|
|
145
|
+
const scale = layerScale * safeScale;
|
|
146
|
+
return Number.isFinite(scale) ? clamp(scale, MIN_LAYER_RESOLUTION_SCALE, 1) : 1;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const shaderCacheKeyForLayer = (layer, shaderName, fragmentShader) => {
|
|
150
|
+
const customSource = typeof layer?.glsl_source === "string" ? layer.glsl_source : null;
|
|
151
|
+
const schemaSignature = stableHash(layer?.param_schema || layer?.params?.param_schema || []);
|
|
152
|
+
if (customSource) {
|
|
153
|
+
return [
|
|
154
|
+
"custom",
|
|
155
|
+
SHADER_CACHE_VERSION,
|
|
156
|
+
String(layer?.glsl || shaderName),
|
|
157
|
+
stableHash(fragmentShader),
|
|
158
|
+
schemaSignature,
|
|
159
|
+
].join(":");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return ["builtin", SHADER_CACHE_VERSION, String(shaderName), schemaSignature].join(":");
|
|
163
|
+
};
|
|
164
|
+
|
|
133
165
|
export const normalizePaletteColors = (value) => {
|
|
134
166
|
const input = Array.isArray(value) ? value : [];
|
|
135
167
|
return input
|
|
@@ -151,10 +183,119 @@ export const parseHexColor = (value) => {
|
|
|
151
183
|
return [0, 2, 4].map((offset) => parseInt(hex.slice(offset, offset + 2), 16) / 255);
|
|
152
184
|
};
|
|
153
185
|
|
|
186
|
+
const normalizePalettePosition = (value, paletteLength) => {
|
|
187
|
+
const numeric = Number(value);
|
|
188
|
+
if (!Number.isFinite(numeric) || !paletteLength) {
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const remainder = numeric % paletteLength;
|
|
193
|
+
return remainder >= 0 ? remainder : remainder + paletteLength;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const interpolatePaletteColor = (left, right, blend) => {
|
|
197
|
+
const ratio = clamp(blend, 0, 1);
|
|
198
|
+
const rgb = left.map((value, index) => {
|
|
199
|
+
const next = right[index];
|
|
200
|
+
return Math.round(clamp(value + (next - value) * ratio, 0, 1) * 255);
|
|
201
|
+
});
|
|
202
|
+
return `#${rgb.map((value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const normalizeGradientStops = (stops, count) => {
|
|
206
|
+
if (!Array.isArray(stops)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const values = stops.map((value) => Number(value)).filter((value) => Number.isFinite(value));
|
|
211
|
+
if (values.length !== count) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return values.map((value) => clamp(value, 0, 1)).sort((left, right) => left - right);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const resolveGradientColor = (value, paletteIndex = 0) => {
|
|
219
|
+
const gradient = value?.gradient || value;
|
|
220
|
+
if (!gradient || typeof gradient !== "object") {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const colors = normalizePaletteColors(gradient?.colors || gradient?.palette);
|
|
225
|
+
if (colors.length === 0) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (colors.length === 1) {
|
|
230
|
+
return colors[0];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const stops = normalizeGradientStops(gradient?.stops || gradient?.stopsPercent, colors.length);
|
|
234
|
+
const basePosition = Number.isFinite(Number(gradient?.position)) ? Number(gradient.position) : Number(paletteIndex);
|
|
235
|
+
|
|
236
|
+
let position;
|
|
237
|
+
if (stops) {
|
|
238
|
+
position = Number.isFinite(basePosition) ? clamp(basePosition, 0, 1) : 0;
|
|
239
|
+
} else {
|
|
240
|
+
const fractional = Number.isFinite(basePosition) ? basePosition - Math.floor(basePosition) : 0;
|
|
241
|
+
position = clamp(fractional, 0, 1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (position <= 0) {
|
|
245
|
+
return colors[0];
|
|
246
|
+
}
|
|
247
|
+
if (position >= 1) {
|
|
248
|
+
return colors[colors.length - 1];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!stops) {
|
|
252
|
+
const segmentLength = 1 / (colors.length - 1);
|
|
253
|
+
const segment = Math.min(Math.floor(position / segmentLength), colors.length - 2);
|
|
254
|
+
const blendBase = segment * segmentLength;
|
|
255
|
+
const blend = (position - blendBase) / segmentLength;
|
|
256
|
+
const left = parseHexColor(colors[segment]);
|
|
257
|
+
const right = parseHexColor(colors[segment + 1]);
|
|
258
|
+
if (!left || !right) {
|
|
259
|
+
return colors[segment];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return interpolatePaletteColor(left, right, blend);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const segment = Array.from({ length: colors.length - 1 }, (_, index) => index)
|
|
266
|
+
.find((index) => position <= stops[index + 1]);
|
|
267
|
+
|
|
268
|
+
if (segment === undefined) {
|
|
269
|
+
return colors[colors.length - 1];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (segment <= 0 && position <= stops[0]) {
|
|
273
|
+
return colors[0];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const start = stops[segment];
|
|
277
|
+
const stop = stops[segment + 1];
|
|
278
|
+
const width = Math.max(stop - start, Number.EPSILON);
|
|
279
|
+
const blend = clamp((position - start) / width, 0, 1);
|
|
280
|
+
const left = parseHexColor(colors[segment]);
|
|
281
|
+
const right = parseHexColor(colors[segment + 1]);
|
|
282
|
+
if (!left || !right) {
|
|
283
|
+
return colors[segment];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return interpolatePaletteColor(left, right, blend);
|
|
287
|
+
};
|
|
288
|
+
|
|
154
289
|
export const resolveLayerCssColor = (params = {}, fallback = "#e5f3ff", paletteIndex = 0) => {
|
|
155
|
-
const explicitColor =
|
|
156
|
-
|
|
157
|
-
|
|
290
|
+
const explicitColor = params?.color;
|
|
291
|
+
const explicitHex = typeof explicitColor === "string" ? String(explicitColor || "").trim() : "";
|
|
292
|
+
if (explicitHex) {
|
|
293
|
+
return explicitHex;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const gradientColor = resolveGradientColor(explicitColor, paletteIndex);
|
|
297
|
+
if (gradientColor) {
|
|
298
|
+
return gradientColor;
|
|
158
299
|
}
|
|
159
300
|
|
|
160
301
|
const palette = normalizePaletteColors(params?.palette);
|
|
@@ -162,7 +303,24 @@ export const resolveLayerCssColor = (params = {}, fallback = "#e5f3ff", paletteI
|
|
|
162
303
|
return fallback;
|
|
163
304
|
}
|
|
164
305
|
|
|
165
|
-
|
|
306
|
+
if (palette.length === 1) {
|
|
307
|
+
return palette[0];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const position = normalizePalettePosition(paletteIndex, palette.length);
|
|
311
|
+
const lowerIndex = Math.floor(position);
|
|
312
|
+
const blend = position - lowerIndex;
|
|
313
|
+
if (blend <= 0 || !Number.isFinite(blend)) {
|
|
314
|
+
return palette[lowerIndex];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const leftColor = parseHexColor(palette[lowerIndex]);
|
|
318
|
+
const rightColor = parseHexColor(palette[(lowerIndex + 1) % palette.length]);
|
|
319
|
+
if (!leftColor || !rightColor) {
|
|
320
|
+
return palette[lowerIndex];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return interpolatePaletteColor(leftColor, rightColor, blend);
|
|
166
324
|
};
|
|
167
325
|
|
|
168
326
|
export const resolveLayerRgbColor = (params = {}, fallback = null, paletteIndex = 0) => {
|
|
@@ -197,11 +355,14 @@ export class LayerManager {
|
|
|
197
355
|
|
|
198
356
|
this.layerFramebuffer = null;
|
|
199
357
|
this.layerTexture = null;
|
|
358
|
+
this.layerTextureSecondary = null;
|
|
200
359
|
this.layerDepthRenderbuffer = null;
|
|
201
360
|
this.layerTargetWidth = 0;
|
|
202
361
|
this.layerTargetHeight = 0;
|
|
203
362
|
this.layerTargetAvailable = true;
|
|
204
363
|
this.layerErrorKeys = new Set();
|
|
364
|
+
this.lastGoodShaderPrograms = new Map();
|
|
365
|
+
this.activeShaderLayerKeys = null;
|
|
205
366
|
|
|
206
367
|
this.particleSystem = new ParticleSystem(this.gl, this.shaderManager);
|
|
207
368
|
this.textRenderer = new TextRenderer(this.gl, this.shaderManager);
|
|
@@ -217,30 +378,35 @@ export class LayerManager {
|
|
|
217
378
|
const layerList = Array.isArray(layers) && layers.length > 0 ? layers : [defaultLayer(audio)];
|
|
218
379
|
const width = Math.max(1, Math.floor(Number(resolution?.[0] || 1)));
|
|
219
380
|
const height = Math.max(1, Math.floor(Number(resolution?.[1] || 1)));
|
|
220
|
-
|
|
381
|
+
const shaderLayerKeys = new Set();
|
|
382
|
+
this.activeShaderLayerKeys = shaderLayerKeys;
|
|
221
383
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
384
|
+
layerList.forEach((layer, index) => {
|
|
385
|
+
try {
|
|
386
|
+
this.ensureLayerTarget(width, height, layer, visualSettings);
|
|
387
|
+
if (!this.layerTargetAvailable || !this.layerFramebuffer || !this.layerTexture) {
|
|
225
388
|
const blend = String(layer?.params?.blend || "alpha").toLowerCase();
|
|
226
389
|
this.setBlendMode(blend);
|
|
227
390
|
this.renderLayer(layer, audio, time, rotation, [width, height], globals, visualSettings, index);
|
|
228
|
-
|
|
229
|
-
this.reportLayerError(layer, error, "direct-render");
|
|
391
|
+
return;
|
|
230
392
|
}
|
|
231
|
-
});
|
|
232
|
-
this.setBlendMode("alpha");
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
393
|
|
|
236
|
-
layerList.forEach((layer, index) => {
|
|
237
|
-
try {
|
|
238
394
|
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.layerFramebuffer);
|
|
239
395
|
this.gl.viewport(0, 0, this.layerTargetWidth, this.layerTargetHeight);
|
|
240
396
|
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
|
241
397
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
242
398
|
|
|
243
|
-
this.renderLayer(
|
|
399
|
+
this.renderLayer(
|
|
400
|
+
layer,
|
|
401
|
+
audio,
|
|
402
|
+
time,
|
|
403
|
+
rotation,
|
|
404
|
+
[this.layerTargetWidth, this.layerTargetHeight],
|
|
405
|
+
globals,
|
|
406
|
+
visualSettings,
|
|
407
|
+
index,
|
|
408
|
+
index
|
|
409
|
+
);
|
|
244
410
|
|
|
245
411
|
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
246
412
|
this.gl.viewport(0, 0, width, height);
|
|
@@ -251,10 +417,22 @@ export class LayerManager {
|
|
|
251
417
|
this.reportLayerError(layer, error, "layer-pass");
|
|
252
418
|
}
|
|
253
419
|
});
|
|
420
|
+
this.pruneLastGoodShaderPrograms(shaderLayerKeys);
|
|
421
|
+
this.activeShaderLayerKeys = null;
|
|
254
422
|
this.setBlendMode("alpha");
|
|
255
423
|
}
|
|
256
424
|
|
|
257
|
-
renderLayer(
|
|
425
|
+
renderLayer(
|
|
426
|
+
layer,
|
|
427
|
+
audio,
|
|
428
|
+
time,
|
|
429
|
+
rotation,
|
|
430
|
+
resolution,
|
|
431
|
+
globals,
|
|
432
|
+
visualSettings,
|
|
433
|
+
paletteIndex = 0,
|
|
434
|
+
layerIndex = null
|
|
435
|
+
) {
|
|
258
436
|
if (isParticleLayer(layer)) {
|
|
259
437
|
this.renderParticleLayer(layer, audio, time, paletteIndex);
|
|
260
438
|
return;
|
|
@@ -280,16 +458,36 @@ export class LayerManager {
|
|
|
280
458
|
return;
|
|
281
459
|
}
|
|
282
460
|
if (isShaderLayer(layer)) {
|
|
283
|
-
this.renderShaderLayer(layer, audio, time, resolution, globals, visualSettings);
|
|
461
|
+
this.renderShaderLayer(layer, audio, time, resolution, globals, visualSettings, paletteIndex, layerIndex);
|
|
284
462
|
return;
|
|
285
463
|
}
|
|
286
|
-
if (this.renderPluginLayer(
|
|
464
|
+
if (this.renderPluginLayer(
|
|
465
|
+
layer,
|
|
466
|
+
audio,
|
|
467
|
+
time,
|
|
468
|
+
rotation,
|
|
469
|
+
resolution,
|
|
470
|
+
globals,
|
|
471
|
+
visualSettings,
|
|
472
|
+
paletteIndex,
|
|
473
|
+
layerIndex
|
|
474
|
+
)) {
|
|
287
475
|
return;
|
|
288
476
|
}
|
|
289
477
|
this.renderGeometryLayer(layer, audio, rotation, time, paletteIndex);
|
|
290
478
|
}
|
|
291
479
|
|
|
292
|
-
renderPluginLayer(
|
|
480
|
+
renderPluginLayer(
|
|
481
|
+
layer,
|
|
482
|
+
audio,
|
|
483
|
+
time,
|
|
484
|
+
rotation,
|
|
485
|
+
resolution,
|
|
486
|
+
globals,
|
|
487
|
+
visualSettings,
|
|
488
|
+
paletteIndex = 0,
|
|
489
|
+
layerIndex = null
|
|
490
|
+
) {
|
|
293
491
|
const context = {
|
|
294
492
|
layer,
|
|
295
493
|
audio,
|
|
@@ -302,19 +500,29 @@ export class LayerManager {
|
|
|
302
500
|
};
|
|
303
501
|
|
|
304
502
|
const renderer = resolveLayerRenderer(layer?.type);
|
|
305
|
-
if (renderer && this.renderPluginOutput(layer, renderer(context), audio, time, resolution, globals, visualSettings, paletteIndex)) {
|
|
503
|
+
if (renderer && this.renderPluginOutput(layer, renderer(context), audio, time, resolution, globals, visualSettings, paletteIndex, layerIndex)) {
|
|
306
504
|
return true;
|
|
307
505
|
}
|
|
308
506
|
|
|
309
507
|
const shaderRenderer = resolveShaderRenderer(layer?.type);
|
|
310
|
-
if (shaderRenderer && this.renderPluginOutput(layer, shaderRenderer(context), audio, time, resolution, globals, visualSettings, paletteIndex)) {
|
|
508
|
+
if (shaderRenderer && this.renderPluginOutput(layer, shaderRenderer(context), audio, time, resolution, globals, visualSettings, paletteIndex, layerIndex)) {
|
|
311
509
|
return true;
|
|
312
510
|
}
|
|
313
511
|
|
|
314
512
|
return false;
|
|
315
513
|
}
|
|
316
514
|
|
|
317
|
-
renderPluginOutput(
|
|
515
|
+
renderPluginOutput(
|
|
516
|
+
layer,
|
|
517
|
+
output,
|
|
518
|
+
audio,
|
|
519
|
+
time,
|
|
520
|
+
resolution,
|
|
521
|
+
globals,
|
|
522
|
+
visualSettings,
|
|
523
|
+
paletteIndex = 0,
|
|
524
|
+
layerIndex = null
|
|
525
|
+
) {
|
|
318
526
|
const lines = normalizePluginLineOutput(output);
|
|
319
527
|
if (lines) {
|
|
320
528
|
const fallbackColor = resolveLayerRgbColor(layer?.params || {}, [0.82, 0.92, 1.0], paletteIndex);
|
|
@@ -332,42 +540,74 @@ export class LayerManager {
|
|
|
332
540
|
shader: layer?.shader || "default",
|
|
333
541
|
glsl: `plugin:${String(layer?.type || "layer")}:${shader.cacheKey}`,
|
|
334
542
|
glsl_source: shader.fragmentShader
|
|
335
|
-
}, audio, time, resolution, globals, visualSettings);
|
|
543
|
+
}, audio, time, resolution, globals, visualSettings, paletteIndex, layerIndex);
|
|
336
544
|
return true;
|
|
337
545
|
}
|
|
338
546
|
|
|
339
|
-
renderShaderLayer(
|
|
547
|
+
renderShaderLayer(
|
|
548
|
+
layer,
|
|
549
|
+
audio,
|
|
550
|
+
time,
|
|
551
|
+
resolution,
|
|
552
|
+
globals,
|
|
553
|
+
visualSettings,
|
|
554
|
+
paletteIndex = 0,
|
|
555
|
+
layerIndex = null
|
|
556
|
+
) {
|
|
340
557
|
const shaderName = String(layer?.shader || "gradient_pulse");
|
|
341
558
|
const customSource = typeof layer?.glsl_source === "string" ? layer.glsl_source : null;
|
|
342
559
|
const fragmentShader = customSource || getBuiltinShader(shaderName);
|
|
343
|
-
const cacheKey =
|
|
344
|
-
|
|
345
|
-
|
|
560
|
+
const cacheKey = shaderCacheKeyForLayer(layer, shaderName, fragmentShader);
|
|
561
|
+
const layerCacheKey = this.shaderLayerCacheKey(layer, layerIndex, customSource);
|
|
562
|
+
if (layerCacheKey) {
|
|
563
|
+
this.activeShaderLayerKeys?.add(layerCacheKey);
|
|
564
|
+
}
|
|
565
|
+
|
|
346
566
|
let program = null;
|
|
347
567
|
try {
|
|
348
568
|
program = this.shaderManager.getProgram(cacheKey, FULLSCREEN_VERTEX_SHADER, fragmentShader);
|
|
569
|
+
if (layerCacheKey) {
|
|
570
|
+
this.lastGoodShaderPrograms.set(layerCacheKey, {
|
|
571
|
+
program,
|
|
572
|
+
cacheKey,
|
|
573
|
+
createdAt: Date.now()
|
|
574
|
+
});
|
|
575
|
+
}
|
|
349
576
|
} catch (error) {
|
|
350
577
|
if (customSource) {
|
|
351
578
|
this.reportShaderError(layer, error, "custom-shader");
|
|
352
579
|
console.warn("Failed to compile custom GLSL, falling back to builtin shader", error);
|
|
580
|
+
const cached = layerCacheKey ? this.lastGoodShaderPrograms.get(layerCacheKey) : null;
|
|
581
|
+
if (cached && cached.program) {
|
|
582
|
+
program = cached.program;
|
|
583
|
+
}
|
|
584
|
+
|
|
353
585
|
try {
|
|
354
|
-
program
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
586
|
+
if (!program) {
|
|
587
|
+
program = this.shaderManager.getProgram(
|
|
588
|
+
shaderCacheKeyForLayer(
|
|
589
|
+
{ ...layer, glsl_source: null },
|
|
590
|
+
shaderName,
|
|
591
|
+
getBuiltinShader(shaderName)
|
|
592
|
+
),
|
|
593
|
+
FULLSCREEN_VERTEX_SHADER,
|
|
594
|
+
getBuiltinShader(shaderName)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
359
597
|
} catch (builtinError) {
|
|
360
598
|
this.reportShaderError(layer, builtinError, "builtin-shader-fallback");
|
|
361
599
|
this.reportLayerError(layer, builtinError, "builtin-shader-fallback");
|
|
600
|
+
program = null;
|
|
362
601
|
}
|
|
363
602
|
} else {
|
|
364
603
|
this.reportShaderError(layer, error, "builtin-shader");
|
|
365
604
|
this.reportLayerError(layer, error, "builtin-shader");
|
|
605
|
+
program = null;
|
|
366
606
|
}
|
|
367
607
|
|
|
368
608
|
if (!program) {
|
|
369
609
|
program = this.shaderManager.getProgram(
|
|
370
|
-
"
|
|
610
|
+
shaderCacheKeyForLayer({ shader: "default" }, "default", getBuiltinShader("default")),
|
|
371
611
|
FULLSCREEN_VERTEX_SHADER,
|
|
372
612
|
getBuiltinShader("default")
|
|
373
613
|
);
|
|
@@ -383,6 +623,7 @@ export class LayerManager {
|
|
|
383
623
|
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
384
624
|
|
|
385
625
|
const bands = audio?.bands || {};
|
|
626
|
+
const bandPeaks = audio?.band_peaks || {};
|
|
386
627
|
const onsets = audio?.onsets || {};
|
|
387
628
|
const drums = audio?.drums || {};
|
|
388
629
|
this.setUniform1f(program, "u_time", time);
|
|
@@ -391,8 +632,19 @@ export class LayerManager {
|
|
|
391
632
|
this.setUniform1f(program, "u_bass", bands.low || 0);
|
|
392
633
|
this.setUniform1f(program, "u_mid", bands.mid || 0);
|
|
393
634
|
this.setUniform1f(program, "u_high", bands.high || 0);
|
|
635
|
+
this.setUniform1f(program, "u_bass_peak", bandPeaks.low || 0);
|
|
636
|
+
this.setUniform1f(program, "u_mid_peak", bandPeaks.mid || 0);
|
|
637
|
+
this.setUniform1f(program, "u_high_peak", bandPeaks.high || 0);
|
|
394
638
|
this.setUniform1f(program, "u_beat", audio?.beat ? 1 : 0);
|
|
395
639
|
this.setUniform1f(program, "u_beat_pulse", audio?.beat_pulse || (audio?.beat ? 1 : 0));
|
|
640
|
+
this.setUniform1f(program, "u_beat_phase", audio?.beat_phase || 0);
|
|
641
|
+
this.setUniform1f(program, "u_bar_phase", audio?.bar_phase || 0);
|
|
642
|
+
this.setUniform1f(program, "u_bar_count", audio?.bar_count || 0);
|
|
643
|
+
this.setUniform1f(program, "u_phrase_count", audio?.phrase_count || 0);
|
|
644
|
+
this.setUniform1f(program, "u_beat_2", audio?.beat_2 ? 1 : 0);
|
|
645
|
+
this.setUniform1f(program, "u_beat_4", audio?.beat_4 ? 1 : 0);
|
|
646
|
+
this.setUniform1f(program, "u_beat_8", audio?.beat_8 ? 1 : 0);
|
|
647
|
+
this.setUniform1f(program, "u_beat_triplet", audio?.beat_triplet ? 1 : 0);
|
|
396
648
|
this.setUniform1f(program, "u_onset", audio?.onset || 0);
|
|
397
649
|
this.setUniform1f(program, "u_sub_onset", onsets.sub || 0);
|
|
398
650
|
this.setUniform1f(program, "u_low_onset", onsets.low || 0);
|
|
@@ -598,30 +850,96 @@ export class LayerManager {
|
|
|
598
850
|
compositeLayer(layer, { audio, time, resolution }) {
|
|
599
851
|
const gl = this.gl;
|
|
600
852
|
const params = layer?.params || {};
|
|
601
|
-
const
|
|
853
|
+
const effects = this.resolvePostEffects(params);
|
|
854
|
+
if (effects.length === 0) {
|
|
855
|
+
this.drawLayerTexture(this.layerTexture, { layer, audio, time, resolution, opacity: params.opacity });
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
602
859
|
const blend = String(params.blend || "alpha").toLowerCase();
|
|
603
|
-
const effectName = String(params.effect || "");
|
|
604
|
-
const vjEffectName = String(params.vj_effect || "");
|
|
605
860
|
const effectIntensity = clamp(Number(params.effect_intensity || audio?.amplitude || 0.35), 0, 1);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
861
|
+
let sourceTexture = this.layerTexture;
|
|
862
|
+
let targetTexture = this.layerTextureSecondary;
|
|
863
|
+
|
|
864
|
+
this.setBlendMode(blend);
|
|
865
|
+
effects.forEach((effectName) => {
|
|
866
|
+
const resolvedName = String(effectName);
|
|
867
|
+
const vjShader = getVJEffectShader(resolvedName);
|
|
868
|
+
const effectShader = getPostEffectShader(resolvedName);
|
|
869
|
+
const selectedShader = vjShader || effectShader;
|
|
870
|
+
const selectedEffectName = vjShader ? `vj:${resolvedName}` : `post:${resolvedName}`;
|
|
871
|
+
let program = this.compositeProgram;
|
|
872
|
+
|
|
873
|
+
if (selectedShader) {
|
|
874
|
+
try {
|
|
875
|
+
program = this.shaderManager.getProgram(
|
|
876
|
+
selectedEffectName,
|
|
877
|
+
FULLSCREEN_VERTEX_SHADER,
|
|
878
|
+
selectedShader
|
|
879
|
+
);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
this.reportLayerError(layer, error, selectedEffectName);
|
|
882
|
+
program = this.compositeProgram;
|
|
883
|
+
}
|
|
621
884
|
}
|
|
885
|
+
|
|
886
|
+
this.applyEffectPass({
|
|
887
|
+
sourceTexture,
|
|
888
|
+
destinationTexture: targetTexture,
|
|
889
|
+
program,
|
|
890
|
+
time,
|
|
891
|
+
resolution: [this.layerTargetWidth, this.layerTargetHeight],
|
|
892
|
+
effectIntensity
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
[sourceTexture, targetTexture] = [targetTexture, sourceTexture];
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
899
|
+
gl.viewport(0, 0, resolution[0], resolution[1]);
|
|
900
|
+
this.drawLayerTexture(sourceTexture, {
|
|
901
|
+
layer,
|
|
902
|
+
audio,
|
|
903
|
+
time,
|
|
904
|
+
resolution,
|
|
905
|
+
opacity: params.opacity
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
resolvePostEffects(params) {
|
|
910
|
+
const postEffects = Array.isArray(params?.post_effects)
|
|
911
|
+
? params.post_effects
|
|
912
|
+
.map((value) => String(value || "").trim().toLowerCase())
|
|
913
|
+
.filter((value) => value.length > 0)
|
|
914
|
+
: [];
|
|
915
|
+
if (postEffects.length > 0) {
|
|
916
|
+
return postEffects;
|
|
622
917
|
}
|
|
623
918
|
|
|
624
|
-
|
|
919
|
+
const vjEffectName = String(params?.vj_effect || "").trim().toLowerCase();
|
|
920
|
+
if (vjEffectName) {
|
|
921
|
+
return [vjEffectName];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const effectName = String(params?.effect || "").trim().toLowerCase();
|
|
925
|
+
if (effectName) {
|
|
926
|
+
return [effectName];
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return [];
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
applyEffectPass({ sourceTexture, destinationTexture, program, time, resolution, effectIntensity }) {
|
|
933
|
+
const gl = this.gl;
|
|
934
|
+
if (!sourceTexture || !destinationTexture) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.layerFramebuffer);
|
|
939
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, destinationTexture, 0);
|
|
940
|
+
gl.viewport(0, 0, this.layerTargetWidth, this.layerTargetHeight);
|
|
941
|
+
gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
|
942
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
625
943
|
|
|
626
944
|
gl.useProgram(program);
|
|
627
945
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.fullscreenBuffer);
|
|
@@ -630,22 +948,41 @@ export class LayerManager {
|
|
|
630
948
|
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
631
949
|
|
|
632
950
|
gl.activeTexture(gl.TEXTURE0);
|
|
633
|
-
gl.bindTexture(gl.TEXTURE_2D,
|
|
951
|
+
gl.bindTexture(gl.TEXTURE_2D, sourceTexture);
|
|
634
952
|
this.setUniform1i(program, "u_texture", 0);
|
|
635
|
-
this.setUniform1f(program, "u_opacity", opacity);
|
|
636
953
|
this.setUniform1f(program, "u_time", time);
|
|
637
954
|
this.setUniform1f(program, "u_intensity", effectIntensity);
|
|
638
955
|
this.setUniform2f(program, "u_resolution", resolution[0], resolution[1]);
|
|
956
|
+
this.setUniform1f(program, "u_opacity", 1);
|
|
639
957
|
|
|
640
958
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
641
959
|
}
|
|
642
960
|
|
|
643
|
-
|
|
961
|
+
drawLayerTexture(texture, { layer, audio, time, resolution, opacity }) {
|
|
962
|
+
const gl = this.gl;
|
|
963
|
+
const params = layer?.params || {};
|
|
964
|
+
gl.useProgram(this.compositeProgram);
|
|
965
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.fullscreenBuffer);
|
|
966
|
+
gl.enableVertexAttribArray(this.compositePositionLocation);
|
|
967
|
+
gl.vertexAttribPointer(this.compositePositionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
968
|
+
|
|
969
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
970
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
971
|
+
this.setUniform1i(this.compositeProgram, "u_texture", 0);
|
|
972
|
+
this.setUniform1f(this.compositeProgram, "u_opacity", clamp(Number(opacity || 1), 0, 1));
|
|
973
|
+
this.setUniform1f(this.compositeProgram, "u_time", time);
|
|
974
|
+
this.setUniform1f(this.compositeProgram, "u_intensity", params.effect_intensity || audio?.amplitude || 0.35);
|
|
975
|
+
this.setUniform2f(this.compositeProgram, "u_resolution", resolution[0], resolution[1]);
|
|
976
|
+
|
|
977
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
ensureLayerTarget(width, height, layer = null, visualSettings = null) {
|
|
644
981
|
if (!this.layerTargetAvailable) {
|
|
645
982
|
return;
|
|
646
983
|
}
|
|
647
984
|
|
|
648
|
-
const [targetWidth, targetHeight] = this.resolveLayerTargetSize(width, height);
|
|
985
|
+
const [targetWidth, targetHeight] = this.resolveLayerTargetSize(width, height, layer, visualSettings);
|
|
649
986
|
if (this.layerFramebuffer && this.layerTargetWidth === targetWidth && this.layerTargetHeight === targetHeight) {
|
|
650
987
|
return;
|
|
651
988
|
}
|
|
@@ -656,16 +993,10 @@ export class LayerManager {
|
|
|
656
993
|
|
|
657
994
|
const gl = this.gl;
|
|
658
995
|
this.layerFramebuffer = gl.createFramebuffer();
|
|
659
|
-
this.layerTexture =
|
|
996
|
+
this.layerTexture = this.createLayerTexture(targetWidth, targetHeight);
|
|
997
|
+
this.layerTextureSecondary = this.createLayerTexture(targetWidth, targetHeight);
|
|
660
998
|
this.layerDepthRenderbuffer = gl.createRenderbuffer();
|
|
661
999
|
|
|
662
|
-
gl.bindTexture(gl.TEXTURE_2D, this.layerTexture);
|
|
663
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, targetWidth, targetHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
664
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
665
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
666
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
667
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
668
|
-
|
|
669
1000
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.layerFramebuffer);
|
|
670
1001
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.layerTexture, 0);
|
|
671
1002
|
|
|
@@ -687,11 +1018,27 @@ export class LayerManager {
|
|
|
687
1018
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
688
1019
|
}
|
|
689
1020
|
|
|
1021
|
+
createLayerTexture(width, height) {
|
|
1022
|
+
const gl = this.gl;
|
|
1023
|
+
const texture = gl.createTexture();
|
|
1024
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1025
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
1026
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
1027
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
1028
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
1029
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
1030
|
+
return texture;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
690
1033
|
disposeLayerTarget() {
|
|
691
1034
|
if (this.layerTexture) {
|
|
692
1035
|
this.gl.deleteTexture(this.layerTexture);
|
|
693
1036
|
this.layerTexture = null;
|
|
694
1037
|
}
|
|
1038
|
+
if (this.layerTextureSecondary) {
|
|
1039
|
+
this.gl.deleteTexture(this.layerTextureSecondary);
|
|
1040
|
+
this.layerTextureSecondary = null;
|
|
1041
|
+
}
|
|
695
1042
|
if (this.layerDepthRenderbuffer) {
|
|
696
1043
|
this.gl.deleteRenderbuffer(this.layerDepthRenderbuffer);
|
|
697
1044
|
this.layerDepthRenderbuffer = null;
|
|
@@ -702,11 +1049,12 @@ export class LayerManager {
|
|
|
702
1049
|
}
|
|
703
1050
|
}
|
|
704
1051
|
|
|
705
|
-
resolveLayerTargetSize(width, height) {
|
|
1052
|
+
resolveLayerTargetSize(width, height, layer = null, visualSettings = null) {
|
|
706
1053
|
const gl = this.gl;
|
|
707
1054
|
const maxTextureSize = Number(gl.getParameter(gl.MAX_TEXTURE_SIZE) || 4096);
|
|
708
|
-
|
|
709
|
-
let
|
|
1055
|
+
const scale = resolveLayerResolutionScale(layer?.params || {}, visualSettings || {});
|
|
1056
|
+
let targetWidth = clamp(Math.floor(width * scale), 1, maxTextureSize);
|
|
1057
|
+
let targetHeight = clamp(Math.floor(height * scale), 1, maxTextureSize);
|
|
710
1058
|
|
|
711
1059
|
const pixels = targetWidth * targetHeight;
|
|
712
1060
|
if (pixels > MAX_LAYER_TARGET_PIXELS) {
|
|
@@ -718,6 +1066,23 @@ export class LayerManager {
|
|
|
718
1066
|
return [targetWidth, targetHeight];
|
|
719
1067
|
}
|
|
720
1068
|
|
|
1069
|
+
dispose() {
|
|
1070
|
+
this.disposeLayerTarget();
|
|
1071
|
+
this.particleSystem?.dispose?.();
|
|
1072
|
+
this.textRenderer?.dispose?.();
|
|
1073
|
+
this.imageRenderer?.dispose?.();
|
|
1074
|
+
this.shapeRenderer?.dispose?.();
|
|
1075
|
+
this.spectrogramRenderer?.dispose?.();
|
|
1076
|
+
if (this.fullscreenBuffer) {
|
|
1077
|
+
this.gl.deleteBuffer(this.fullscreenBuffer);
|
|
1078
|
+
this.fullscreenBuffer = null;
|
|
1079
|
+
}
|
|
1080
|
+
if (this.geometryBuffer) {
|
|
1081
|
+
this.gl.deleteBuffer(this.geometryBuffer);
|
|
1082
|
+
this.geometryBuffer = null;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
721
1086
|
setBlendMode(mode) {
|
|
722
1087
|
const blendMode = normalizeBlendMode(mode);
|
|
723
1088
|
this.gl.blendEquation(this.gl.FUNC_ADD);
|
|
@@ -794,6 +1159,27 @@ export class LayerManager {
|
|
|
794
1159
|
window.dispatchEvent(new CustomEvent(SHADER_ERROR_EVENT, { detail }));
|
|
795
1160
|
}
|
|
796
1161
|
}
|
|
1162
|
+
|
|
1163
|
+
pruneLastGoodShaderPrograms(activeKeys) {
|
|
1164
|
+
const active = activeKeys instanceof Set ? activeKeys : null;
|
|
1165
|
+
if (!active) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
for (const key of this.lastGoodShaderPrograms.keys()) {
|
|
1169
|
+
if (!active.has(key)) {
|
|
1170
|
+
this.lastGoodShaderPrograms.delete(key);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
shaderLayerCacheKey(layer, layerIndex, hasCustomSource) {
|
|
1176
|
+
const name = String(layer?.name || "unnamed");
|
|
1177
|
+
const type = String(layer?.type || "layer");
|
|
1178
|
+
const sourceKind = hasCustomSource ? "custom" : "builtin";
|
|
1179
|
+
const sourceId = String(layer?.glsl || layer?.shader || layer?.type || "default");
|
|
1180
|
+
const index = Number.isFinite(layerIndex) ? layerIndex : "static";
|
|
1181
|
+
return `shader-layer|${sourceKind}|${index}|${type}|${name}|${sourceId}`;
|
|
1182
|
+
}
|
|
797
1183
|
}
|
|
798
1184
|
|
|
799
1185
|
const isShaderLayer = (layer) => {
|
|
@@ -859,10 +1245,25 @@ const defaultLayer = (audio) => ({
|
|
|
859
1245
|
|
|
860
1246
|
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
861
1247
|
|
|
1248
|
+
const stableHash = (value) => hashString(stableStringify(value));
|
|
1249
|
+
|
|
1250
|
+
const stableStringify = (value) => {
|
|
1251
|
+
if (value === null || typeof value !== "object") {
|
|
1252
|
+
return String(value);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if (Array.isArray(value)) {
|
|
1256
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
return `{${Object.keys(value).sort().map((key) => `${key}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1260
|
+
};
|
|
1261
|
+
|
|
862
1262
|
const hashString = (value) => {
|
|
1263
|
+
const text = String(value || "");
|
|
863
1264
|
let hash = 0;
|
|
864
|
-
for (let index = 0; index <
|
|
865
|
-
hash = (hash * 31 +
|
|
1265
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1266
|
+
hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
|
|
866
1267
|
}
|
|
867
1268
|
return hash.toString(16);
|
|
868
1269
|
};
|