vizcore 1.0.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/README.md +66 -648
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/index.html +2 -1
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/frontend/index.html +50 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +703 -45
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +401 -11
- data/frontend/src/renderer/layer-manager.js +490 -75
- 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/shape-editor-controls.js +157 -0
- data/frontend/src/visuals/geometry.js +425 -27
- 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 +488 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -0
- data/frontend/src/visuals/svg-arc.js +104 -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 +65 -9
- 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 +573 -33
- 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 +1072 -21
- 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 +549 -13
- 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 +5 -2
- 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 +190 -12
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +641 -23
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +513 -18
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +697 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +742 -0
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +34 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +154 -4
- metadata +29 -3
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
resolveShaderRenderer
|
|
19
19
|
} from "../plugin-runtime.js";
|
|
20
20
|
import { SpectrogramRenderer } from "../visuals/spectrogram-renderer.js";
|
|
21
|
+
import { ShapeRenderer } from "../visuals/shape-renderer.js";
|
|
21
22
|
import { TextRenderer } from "../visuals/text-renderer.js";
|
|
22
23
|
import { getVJEffectShader } from "../visuals/vj-effects.js";
|
|
23
24
|
import { FULLSCREEN_VERTEX_SHADER } from "./shader-manager.js";
|
|
@@ -57,6 +58,8 @@ const FULLSCREEN_VERTICES = new Float32Array([
|
|
|
57
58
|
1.0, 1.0
|
|
58
59
|
]);
|
|
59
60
|
const MAX_LAYER_TARGET_PIXELS = 4_194_304;
|
|
61
|
+
const MIN_LAYER_RESOLUTION_SCALE = 0.1;
|
|
62
|
+
const SHADER_CACHE_VERSION = "v2";
|
|
60
63
|
|
|
61
64
|
export const coerceUniformNumber = (value) => {
|
|
62
65
|
if (typeof value === "boolean") {
|
|
@@ -129,6 +132,36 @@ export const normalizeBlendMode = (mode) => {
|
|
|
129
132
|
return "alpha";
|
|
130
133
|
};
|
|
131
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
|
+
|
|
132
165
|
export const normalizePaletteColors = (value) => {
|
|
133
166
|
const input = Array.isArray(value) ? value : [];
|
|
134
167
|
return input
|
|
@@ -150,10 +183,119 @@ export const parseHexColor = (value) => {
|
|
|
150
183
|
return [0, 2, 4].map((offset) => parseInt(hex.slice(offset, offset + 2), 16) / 255);
|
|
151
184
|
};
|
|
152
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
|
+
|
|
153
289
|
export const resolveLayerCssColor = (params = {}, fallback = "#e5f3ff", paletteIndex = 0) => {
|
|
154
|
-
const explicitColor =
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
157
299
|
}
|
|
158
300
|
|
|
159
301
|
const palette = normalizePaletteColors(params?.palette);
|
|
@@ -161,7 +303,24 @@ export const resolveLayerCssColor = (params = {}, fallback = "#e5f3ff", paletteI
|
|
|
161
303
|
return fallback;
|
|
162
304
|
}
|
|
163
305
|
|
|
164
|
-
|
|
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);
|
|
165
324
|
};
|
|
166
325
|
|
|
167
326
|
export const resolveLayerRgbColor = (params = {}, fallback = null, paletteIndex = 0) => {
|
|
@@ -196,15 +355,19 @@ export class LayerManager {
|
|
|
196
355
|
|
|
197
356
|
this.layerFramebuffer = null;
|
|
198
357
|
this.layerTexture = null;
|
|
358
|
+
this.layerTextureSecondary = null;
|
|
199
359
|
this.layerDepthRenderbuffer = null;
|
|
200
360
|
this.layerTargetWidth = 0;
|
|
201
361
|
this.layerTargetHeight = 0;
|
|
202
362
|
this.layerTargetAvailable = true;
|
|
203
363
|
this.layerErrorKeys = new Set();
|
|
364
|
+
this.lastGoodShaderPrograms = new Map();
|
|
365
|
+
this.activeShaderLayerKeys = null;
|
|
204
366
|
|
|
205
367
|
this.particleSystem = new ParticleSystem(this.gl, this.shaderManager);
|
|
206
368
|
this.textRenderer = new TextRenderer(this.gl, this.shaderManager);
|
|
207
369
|
this.imageRenderer = new ImageRenderer(this.gl, this.shaderManager);
|
|
370
|
+
this.shapeRenderer = new ShapeRenderer(this.gl, this.shaderManager);
|
|
208
371
|
this.spectrogramRenderer = new SpectrogramRenderer(this.gl, this.shaderManager);
|
|
209
372
|
|
|
210
373
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.fullscreenBuffer);
|
|
@@ -215,30 +378,35 @@ export class LayerManager {
|
|
|
215
378
|
const layerList = Array.isArray(layers) && layers.length > 0 ? layers : [defaultLayer(audio)];
|
|
216
379
|
const width = Math.max(1, Math.floor(Number(resolution?.[0] || 1)));
|
|
217
380
|
const height = Math.max(1, Math.floor(Number(resolution?.[1] || 1)));
|
|
218
|
-
|
|
381
|
+
const shaderLayerKeys = new Set();
|
|
382
|
+
this.activeShaderLayerKeys = shaderLayerKeys;
|
|
219
383
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
384
|
+
layerList.forEach((layer, index) => {
|
|
385
|
+
try {
|
|
386
|
+
this.ensureLayerTarget(width, height, layer, visualSettings);
|
|
387
|
+
if (!this.layerTargetAvailable || !this.layerFramebuffer || !this.layerTexture) {
|
|
223
388
|
const blend = String(layer?.params?.blend || "alpha").toLowerCase();
|
|
224
389
|
this.setBlendMode(blend);
|
|
225
390
|
this.renderLayer(layer, audio, time, rotation, [width, height], globals, visualSettings, index);
|
|
226
|
-
|
|
227
|
-
this.reportLayerError(layer, error, "direct-render");
|
|
391
|
+
return;
|
|
228
392
|
}
|
|
229
|
-
});
|
|
230
|
-
this.setBlendMode("alpha");
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
393
|
|
|
234
|
-
layerList.forEach((layer, index) => {
|
|
235
|
-
try {
|
|
236
394
|
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.layerFramebuffer);
|
|
237
395
|
this.gl.viewport(0, 0, this.layerTargetWidth, this.layerTargetHeight);
|
|
238
396
|
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
|
239
397
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
240
398
|
|
|
241
|
-
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
|
+
);
|
|
242
410
|
|
|
243
411
|
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
244
412
|
this.gl.viewport(0, 0, width, height);
|
|
@@ -249,10 +417,22 @@ export class LayerManager {
|
|
|
249
417
|
this.reportLayerError(layer, error, "layer-pass");
|
|
250
418
|
}
|
|
251
419
|
});
|
|
420
|
+
this.pruneLastGoodShaderPrograms(shaderLayerKeys);
|
|
421
|
+
this.activeShaderLayerKeys = null;
|
|
252
422
|
this.setBlendMode("alpha");
|
|
253
423
|
}
|
|
254
424
|
|
|
255
|
-
renderLayer(
|
|
425
|
+
renderLayer(
|
|
426
|
+
layer,
|
|
427
|
+
audio,
|
|
428
|
+
time,
|
|
429
|
+
rotation,
|
|
430
|
+
resolution,
|
|
431
|
+
globals,
|
|
432
|
+
visualSettings,
|
|
433
|
+
paletteIndex = 0,
|
|
434
|
+
layerIndex = null
|
|
435
|
+
) {
|
|
256
436
|
if (isParticleLayer(layer)) {
|
|
257
437
|
this.renderParticleLayer(layer, audio, time, paletteIndex);
|
|
258
438
|
return;
|
|
@@ -274,20 +454,40 @@ export class LayerManager {
|
|
|
274
454
|
return;
|
|
275
455
|
}
|
|
276
456
|
if (isShapeLayer(layer)) {
|
|
277
|
-
this.renderShapeLayer(layer, audio, paletteIndex);
|
|
457
|
+
this.renderShapeLayer(layer, audio, time, resolution, paletteIndex);
|
|
278
458
|
return;
|
|
279
459
|
}
|
|
280
460
|
if (isShaderLayer(layer)) {
|
|
281
|
-
this.renderShaderLayer(layer, audio, time, resolution, globals, visualSettings);
|
|
461
|
+
this.renderShaderLayer(layer, audio, time, resolution, globals, visualSettings, paletteIndex, layerIndex);
|
|
282
462
|
return;
|
|
283
463
|
}
|
|
284
|
-
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
|
+
)) {
|
|
285
475
|
return;
|
|
286
476
|
}
|
|
287
477
|
this.renderGeometryLayer(layer, audio, rotation, time, paletteIndex);
|
|
288
478
|
}
|
|
289
479
|
|
|
290
|
-
renderPluginLayer(
|
|
480
|
+
renderPluginLayer(
|
|
481
|
+
layer,
|
|
482
|
+
audio,
|
|
483
|
+
time,
|
|
484
|
+
rotation,
|
|
485
|
+
resolution,
|
|
486
|
+
globals,
|
|
487
|
+
visualSettings,
|
|
488
|
+
paletteIndex = 0,
|
|
489
|
+
layerIndex = null
|
|
490
|
+
) {
|
|
291
491
|
const context = {
|
|
292
492
|
layer,
|
|
293
493
|
audio,
|
|
@@ -300,19 +500,29 @@ export class LayerManager {
|
|
|
300
500
|
};
|
|
301
501
|
|
|
302
502
|
const renderer = resolveLayerRenderer(layer?.type);
|
|
303
|
-
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)) {
|
|
304
504
|
return true;
|
|
305
505
|
}
|
|
306
506
|
|
|
307
507
|
const shaderRenderer = resolveShaderRenderer(layer?.type);
|
|
308
|
-
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)) {
|
|
309
509
|
return true;
|
|
310
510
|
}
|
|
311
511
|
|
|
312
512
|
return false;
|
|
313
513
|
}
|
|
314
514
|
|
|
315
|
-
renderPluginOutput(
|
|
515
|
+
renderPluginOutput(
|
|
516
|
+
layer,
|
|
517
|
+
output,
|
|
518
|
+
audio,
|
|
519
|
+
time,
|
|
520
|
+
resolution,
|
|
521
|
+
globals,
|
|
522
|
+
visualSettings,
|
|
523
|
+
paletteIndex = 0,
|
|
524
|
+
layerIndex = null
|
|
525
|
+
) {
|
|
316
526
|
const lines = normalizePluginLineOutput(output);
|
|
317
527
|
if (lines) {
|
|
318
528
|
const fallbackColor = resolveLayerRgbColor(layer?.params || {}, [0.82, 0.92, 1.0], paletteIndex);
|
|
@@ -330,42 +540,74 @@ export class LayerManager {
|
|
|
330
540
|
shader: layer?.shader || "default",
|
|
331
541
|
glsl: `plugin:${String(layer?.type || "layer")}:${shader.cacheKey}`,
|
|
332
542
|
glsl_source: shader.fragmentShader
|
|
333
|
-
}, audio, time, resolution, globals, visualSettings);
|
|
543
|
+
}, audio, time, resolution, globals, visualSettings, paletteIndex, layerIndex);
|
|
334
544
|
return true;
|
|
335
545
|
}
|
|
336
546
|
|
|
337
|
-
renderShaderLayer(
|
|
547
|
+
renderShaderLayer(
|
|
548
|
+
layer,
|
|
549
|
+
audio,
|
|
550
|
+
time,
|
|
551
|
+
resolution,
|
|
552
|
+
globals,
|
|
553
|
+
visualSettings,
|
|
554
|
+
paletteIndex = 0,
|
|
555
|
+
layerIndex = null
|
|
556
|
+
) {
|
|
338
557
|
const shaderName = String(layer?.shader || "gradient_pulse");
|
|
339
558
|
const customSource = typeof layer?.glsl_source === "string" ? layer.glsl_source : null;
|
|
340
559
|
const fragmentShader = customSource || getBuiltinShader(shaderName);
|
|
341
|
-
const cacheKey =
|
|
342
|
-
|
|
343
|
-
|
|
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
|
+
|
|
344
566
|
let program = null;
|
|
345
567
|
try {
|
|
346
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
|
+
}
|
|
347
576
|
} catch (error) {
|
|
348
577
|
if (customSource) {
|
|
349
578
|
this.reportShaderError(layer, error, "custom-shader");
|
|
350
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
|
+
|
|
351
585
|
try {
|
|
352
|
-
program
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
}
|
|
357
597
|
} catch (builtinError) {
|
|
358
598
|
this.reportShaderError(layer, builtinError, "builtin-shader-fallback");
|
|
359
599
|
this.reportLayerError(layer, builtinError, "builtin-shader-fallback");
|
|
600
|
+
program = null;
|
|
360
601
|
}
|
|
361
602
|
} else {
|
|
362
603
|
this.reportShaderError(layer, error, "builtin-shader");
|
|
363
604
|
this.reportLayerError(layer, error, "builtin-shader");
|
|
605
|
+
program = null;
|
|
364
606
|
}
|
|
365
607
|
|
|
366
608
|
if (!program) {
|
|
367
609
|
program = this.shaderManager.getProgram(
|
|
368
|
-
"
|
|
610
|
+
shaderCacheKeyForLayer({ shader: "default" }, "default", getBuiltinShader("default")),
|
|
369
611
|
FULLSCREEN_VERTEX_SHADER,
|
|
370
612
|
getBuiltinShader("default")
|
|
371
613
|
);
|
|
@@ -381,6 +623,7 @@ export class LayerManager {
|
|
|
381
623
|
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
382
624
|
|
|
383
625
|
const bands = audio?.bands || {};
|
|
626
|
+
const bandPeaks = audio?.band_peaks || {};
|
|
384
627
|
const onsets = audio?.onsets || {};
|
|
385
628
|
const drums = audio?.drums || {};
|
|
386
629
|
this.setUniform1f(program, "u_time", time);
|
|
@@ -389,8 +632,19 @@ export class LayerManager {
|
|
|
389
632
|
this.setUniform1f(program, "u_bass", bands.low || 0);
|
|
390
633
|
this.setUniform1f(program, "u_mid", bands.mid || 0);
|
|
391
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);
|
|
392
638
|
this.setUniform1f(program, "u_beat", audio?.beat ? 1 : 0);
|
|
393
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);
|
|
394
648
|
this.setUniform1f(program, "u_onset", audio?.onset || 0);
|
|
395
649
|
this.setUniform1f(program, "u_sub_onset", onsets.sub || 0);
|
|
396
650
|
this.setUniform1f(program, "u_low_onset", onsets.low || 0);
|
|
@@ -556,16 +810,28 @@ export class LayerManager {
|
|
|
556
810
|
});
|
|
557
811
|
}
|
|
558
812
|
|
|
559
|
-
renderShapeLayer(layer, audio, paletteIndex = 0) {
|
|
813
|
+
renderShapeLayer(layer, audio, time, resolution, paletteIndex = 0) {
|
|
560
814
|
const params = layer?.params || {};
|
|
815
|
+
const amplitude = clamp(Number(audio?.amplitude || 0), 0, 1);
|
|
816
|
+
const fallbackColor = [0.85, 0.50 + amplitude * 0.24, 0.95];
|
|
817
|
+
const cssColor = resolveLayerCssColor(params, "#d98cff", paletteIndex);
|
|
818
|
+
const rendered = this.shapeRenderer.render({
|
|
819
|
+
params,
|
|
820
|
+
color: cssColor,
|
|
821
|
+
resolution,
|
|
822
|
+
audio,
|
|
823
|
+
time
|
|
824
|
+
});
|
|
825
|
+
if (rendered) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
561
829
|
const points = buildShapeLines({ params });
|
|
562
830
|
|
|
563
831
|
if (points.length === 0) {
|
|
564
832
|
return;
|
|
565
833
|
}
|
|
566
834
|
|
|
567
|
-
const amplitude = clamp(Number(audio?.amplitude || 0), 0, 1);
|
|
568
|
-
const fallbackColor = [0.85, 0.50 + amplitude * 0.24, 0.95];
|
|
569
835
|
const color = resolveLayerRgbColor(params, fallbackColor, paletteIndex);
|
|
570
836
|
this.renderLinePoints(points, color);
|
|
571
837
|
}
|
|
@@ -584,30 +850,96 @@ export class LayerManager {
|
|
|
584
850
|
compositeLayer(layer, { audio, time, resolution }) {
|
|
585
851
|
const gl = this.gl;
|
|
586
852
|
const params = layer?.params || {};
|
|
587
|
-
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
|
+
|
|
588
859
|
const blend = String(params.blend || "alpha").toLowerCase();
|
|
589
|
-
const effectName = String(params.effect || "");
|
|
590
|
-
const vjEffectName = String(params.vj_effect || "");
|
|
591
860
|
const effectIntensity = clamp(Number(params.effect_intensity || audio?.amplitude || 0.35), 0, 1);
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
+
}
|
|
607
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;
|
|
608
917
|
}
|
|
609
918
|
|
|
610
|
-
|
|
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);
|
|
611
943
|
|
|
612
944
|
gl.useProgram(program);
|
|
613
945
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.fullscreenBuffer);
|
|
@@ -616,22 +948,41 @@ export class LayerManager {
|
|
|
616
948
|
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
|
|
617
949
|
|
|
618
950
|
gl.activeTexture(gl.TEXTURE0);
|
|
619
|
-
gl.bindTexture(gl.TEXTURE_2D,
|
|
951
|
+
gl.bindTexture(gl.TEXTURE_2D, sourceTexture);
|
|
620
952
|
this.setUniform1i(program, "u_texture", 0);
|
|
621
|
-
this.setUniform1f(program, "u_opacity", opacity);
|
|
622
953
|
this.setUniform1f(program, "u_time", time);
|
|
623
954
|
this.setUniform1f(program, "u_intensity", effectIntensity);
|
|
624
955
|
this.setUniform2f(program, "u_resolution", resolution[0], resolution[1]);
|
|
956
|
+
this.setUniform1f(program, "u_opacity", 1);
|
|
625
957
|
|
|
626
958
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
627
959
|
}
|
|
628
960
|
|
|
629
|
-
|
|
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) {
|
|
630
981
|
if (!this.layerTargetAvailable) {
|
|
631
982
|
return;
|
|
632
983
|
}
|
|
633
984
|
|
|
634
|
-
const [targetWidth, targetHeight] = this.resolveLayerTargetSize(width, height);
|
|
985
|
+
const [targetWidth, targetHeight] = this.resolveLayerTargetSize(width, height, layer, visualSettings);
|
|
635
986
|
if (this.layerFramebuffer && this.layerTargetWidth === targetWidth && this.layerTargetHeight === targetHeight) {
|
|
636
987
|
return;
|
|
637
988
|
}
|
|
@@ -642,16 +993,10 @@ export class LayerManager {
|
|
|
642
993
|
|
|
643
994
|
const gl = this.gl;
|
|
644
995
|
this.layerFramebuffer = gl.createFramebuffer();
|
|
645
|
-
this.layerTexture =
|
|
996
|
+
this.layerTexture = this.createLayerTexture(targetWidth, targetHeight);
|
|
997
|
+
this.layerTextureSecondary = this.createLayerTexture(targetWidth, targetHeight);
|
|
646
998
|
this.layerDepthRenderbuffer = gl.createRenderbuffer();
|
|
647
999
|
|
|
648
|
-
gl.bindTexture(gl.TEXTURE_2D, this.layerTexture);
|
|
649
|
-
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, targetWidth, targetHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
650
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
651
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
652
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
653
|
-
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
654
|
-
|
|
655
1000
|
gl.bindFramebuffer(gl.FRAMEBUFFER, this.layerFramebuffer);
|
|
656
1001
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.layerTexture, 0);
|
|
657
1002
|
|
|
@@ -673,11 +1018,27 @@ export class LayerManager {
|
|
|
673
1018
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
674
1019
|
}
|
|
675
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
|
+
|
|
676
1033
|
disposeLayerTarget() {
|
|
677
1034
|
if (this.layerTexture) {
|
|
678
1035
|
this.gl.deleteTexture(this.layerTexture);
|
|
679
1036
|
this.layerTexture = null;
|
|
680
1037
|
}
|
|
1038
|
+
if (this.layerTextureSecondary) {
|
|
1039
|
+
this.gl.deleteTexture(this.layerTextureSecondary);
|
|
1040
|
+
this.layerTextureSecondary = null;
|
|
1041
|
+
}
|
|
681
1042
|
if (this.layerDepthRenderbuffer) {
|
|
682
1043
|
this.gl.deleteRenderbuffer(this.layerDepthRenderbuffer);
|
|
683
1044
|
this.layerDepthRenderbuffer = null;
|
|
@@ -688,11 +1049,12 @@ export class LayerManager {
|
|
|
688
1049
|
}
|
|
689
1050
|
}
|
|
690
1051
|
|
|
691
|
-
resolveLayerTargetSize(width, height) {
|
|
1052
|
+
resolveLayerTargetSize(width, height, layer = null, visualSettings = null) {
|
|
692
1053
|
const gl = this.gl;
|
|
693
1054
|
const maxTextureSize = Number(gl.getParameter(gl.MAX_TEXTURE_SIZE) || 4096);
|
|
694
|
-
|
|
695
|
-
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);
|
|
696
1058
|
|
|
697
1059
|
const pixels = targetWidth * targetHeight;
|
|
698
1060
|
if (pixels > MAX_LAYER_TARGET_PIXELS) {
|
|
@@ -704,6 +1066,23 @@ export class LayerManager {
|
|
|
704
1066
|
return [targetWidth, targetHeight];
|
|
705
1067
|
}
|
|
706
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
|
+
|
|
707
1086
|
setBlendMode(mode) {
|
|
708
1087
|
const blendMode = normalizeBlendMode(mode);
|
|
709
1088
|
this.gl.blendEquation(this.gl.FUNC_ADD);
|
|
@@ -780,6 +1159,27 @@ export class LayerManager {
|
|
|
780
1159
|
window.dispatchEvent(new CustomEvent(SHADER_ERROR_EVENT, { detail }));
|
|
781
1160
|
}
|
|
782
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
|
+
}
|
|
783
1183
|
}
|
|
784
1184
|
|
|
785
1185
|
const isShaderLayer = (layer) => {
|
|
@@ -845,10 +1245,25 @@ const defaultLayer = (audio) => ({
|
|
|
845
1245
|
|
|
846
1246
|
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
847
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
|
+
|
|
848
1262
|
const hashString = (value) => {
|
|
1263
|
+
const text = String(value || "");
|
|
849
1264
|
let hash = 0;
|
|
850
|
-
for (let index = 0; index <
|
|
851
|
-
hash = (hash * 31 +
|
|
1265
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1266
|
+
hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
|
|
852
1267
|
}
|
|
853
1268
|
return hash.toString(16);
|
|
854
1269
|
};
|