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
data/frontend/src/main.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { BAND_KEYS, DEFAULT_FFT_BINS, buildAudioInspectorState, formatMeterValue } from "./audio-inspector.js";
|
|
2
2
|
import {
|
|
3
3
|
createLiveControlState,
|
|
4
|
+
isLiveControlEnabled,
|
|
4
5
|
isTapTempoShortcut,
|
|
5
6
|
keyboardActionForKey,
|
|
6
7
|
liveControlStatusText,
|
|
7
8
|
normalizeKeyboardMappings,
|
|
9
|
+
normalizeLiveControlPayload,
|
|
8
10
|
shortcutActionForKey,
|
|
9
11
|
shortcutSceneIndexForKey,
|
|
10
12
|
toggleLiveControl,
|
|
@@ -15,7 +17,10 @@ import {
|
|
|
15
17
|
recordConnectionStatus,
|
|
16
18
|
recordLatencyProbe,
|
|
17
19
|
recordRenderFrame,
|
|
20
|
+
recordRendererCapabilities,
|
|
21
|
+
recordRendererSafeMode,
|
|
18
22
|
recordShaderCompile,
|
|
23
|
+
recordWebSocketBackpressure,
|
|
19
24
|
recordSocketFrame,
|
|
20
25
|
} from "./performance-monitor.js";
|
|
21
26
|
import {
|
|
@@ -29,12 +34,26 @@ import {
|
|
|
29
34
|
upsertMidiLearnBinding,
|
|
30
35
|
} from "./midi-learn.js";
|
|
31
36
|
import { applyProjectorMode, resolveProjectorMode } from "./projector-mode.js";
|
|
32
|
-
import { Engine } from "./renderer/engine.js";
|
|
37
|
+
import { Engine, RENDERER_CAPABILITIES_EVENT, RENDERER_SAFE_MODE_EVENT } from "./renderer/engine.js";
|
|
33
38
|
import { SHADER_COMPILE_EVENT } from "./renderer/shader-manager.js";
|
|
39
|
+
import {
|
|
40
|
+
customShapeParamControlEntries,
|
|
41
|
+
customShapeParamMessage,
|
|
42
|
+
pruneCustomShapeParamOverrides
|
|
43
|
+
} from "./custom-shape-param-controls.js";
|
|
44
|
+
import {
|
|
45
|
+
mappingTargetOptions,
|
|
46
|
+
mappingTargetSignature
|
|
47
|
+
} from "./mapping-target-selector.js";
|
|
34
48
|
import {
|
|
35
49
|
pruneShaderParamOverrides,
|
|
36
50
|
shaderParamControlEntries
|
|
37
51
|
} from "./shader-param-controls.js";
|
|
52
|
+
import {
|
|
53
|
+
normalizeShapeEditorPatch,
|
|
54
|
+
pruneShapeEditorOverrides,
|
|
55
|
+
shapeEditorEntries
|
|
56
|
+
} from "./shape-editor-controls.js";
|
|
38
57
|
import { normalizeRuntimeControlPreset } from "./runtime-control-preset.js";
|
|
39
58
|
import { SHADER_ERROR_EVENT, formatShaderErrorMessage, formatShaderErrorTitle } from "./shader-error-overlay.js";
|
|
40
59
|
import {
|
|
@@ -44,6 +63,7 @@ import {
|
|
|
44
63
|
saveVisualSettingsPreset,
|
|
45
64
|
visualSettingFromUnit
|
|
46
65
|
} from "./visual-settings-preset.js";
|
|
66
|
+
import { applyScenePayload, resolveScenePayload } from "./scene-patches.js";
|
|
47
67
|
import { WebSocketClient } from "./websocket-client.js";
|
|
48
68
|
|
|
49
69
|
window.__vizcoreMainStarted = true;
|
|
@@ -53,6 +73,7 @@ const wsStatusElement = document.querySelector("#ws-status");
|
|
|
53
73
|
const sceneStatusElement = document.querySelector("#scene-status");
|
|
54
74
|
const transitionStatusElement = document.querySelector("#transition-status");
|
|
55
75
|
const frameStatusElement = document.querySelector("#frame-status");
|
|
76
|
+
const runtimeErrorStatusElement = document.querySelector("#runtime-error-status");
|
|
56
77
|
const bpmStatusElement = document.querySelector("#bpm-status");
|
|
57
78
|
const beatStatusElement = document.querySelector("#beat-status");
|
|
58
79
|
const blackoutButton = document.querySelector("#blackout-toggle");
|
|
@@ -73,6 +94,7 @@ const inspectorBandElements = Object.fromEntries(
|
|
|
73
94
|
);
|
|
74
95
|
const fftPreviewElement = document.querySelector("#fft-preview");
|
|
75
96
|
const audioSourceStatusElement = document.querySelector("#audio-source-status");
|
|
97
|
+
const audioHealthStatusElement = document.querySelector("#audio-health-status");
|
|
76
98
|
const audioTrackStatusElement = document.querySelector("#audio-track-status");
|
|
77
99
|
const audioPlaybackStatusElement = document.querySelector("#audio-playback-status");
|
|
78
100
|
const sceneSwitcherElement = document.querySelector("#scene-switcher");
|
|
@@ -91,6 +113,9 @@ const reactivityStatusElement = document.querySelector("#reactivity-status");
|
|
|
91
113
|
const midiLearnStatusElement = document.querySelector("#midi-learn-status");
|
|
92
114
|
const midiLearnButtons = Array.from(document.querySelectorAll("[data-midi-learn-action]"));
|
|
93
115
|
const shaderParamControlsElement = document.querySelector("#shader-param-controls");
|
|
116
|
+
const shapeEditorControlsElement = document.querySelector("#shape-editor-controls");
|
|
117
|
+
const customShapeParamControlsElement = document.querySelector("#custom-shape-param-controls");
|
|
118
|
+
const mappingTargetSelectorElement = document.querySelector("#mapping-target-selector");
|
|
94
119
|
const shaderErrorOverlay = document.querySelector("#shader-error-overlay");
|
|
95
120
|
const shaderErrorTitleElement = document.querySelector("#shader-error-title");
|
|
96
121
|
const shaderErrorMessageElement = document.querySelector("#shader-error-message");
|
|
@@ -116,15 +141,27 @@ let pendingSceneRequestedAt = 0;
|
|
|
116
141
|
let tapTempoKey = null;
|
|
117
142
|
let runtimeGlobalsReceived = false;
|
|
118
143
|
let runtimeControlPresetApplied = false;
|
|
144
|
+
let runtimeControlPresetSceneApplied = null;
|
|
145
|
+
let runtimeControlPresetSceneOverrides = {};
|
|
146
|
+
let runtimeControlPresetVisualBase = null;
|
|
147
|
+
let runtimeControlPresetMidiBase = null;
|
|
119
148
|
let controlPresetSaveUrl = null;
|
|
120
149
|
let shaderParamOverrides = {};
|
|
150
|
+
let scenePayload = null;
|
|
121
151
|
let shaderParamControlsSignature = "";
|
|
152
|
+
let shapeEditorOverrides = {};
|
|
153
|
+
let shapeEditorControlsSignature = "";
|
|
154
|
+
let customShapeParamOverrides = {};
|
|
155
|
+
let customShapeParamControlsSignature = "";
|
|
156
|
+
let mappingTargetSelectorSignature = "";
|
|
157
|
+
let selectedMappingTarget = "";
|
|
122
158
|
let midiAccess = null;
|
|
123
159
|
let pendingMidiLearnAction = null;
|
|
124
160
|
applyProjectorMode(document.body, projectorMode);
|
|
125
161
|
const engine = new Engine(canvas);
|
|
126
162
|
let rendererReady = false;
|
|
127
163
|
bindShaderCompileMetrics();
|
|
164
|
+
bindRendererMetrics();
|
|
128
165
|
try {
|
|
129
166
|
engine.init();
|
|
130
167
|
rendererReady = true;
|
|
@@ -160,10 +197,26 @@ startPerformanceMonitorLoop();
|
|
|
160
197
|
const websocketUrl = buildWebSocketUrl();
|
|
161
198
|
const client = new WebSocketClient(websocketUrl, {
|
|
162
199
|
onFrame: (frame) => {
|
|
200
|
+
const resolvedScene = resolveScenePayload({
|
|
201
|
+
incomingScene: frame?.scene,
|
|
202
|
+
currentScene: scenePayload,
|
|
203
|
+
frameVersion: frame?.scene_version,
|
|
204
|
+
});
|
|
205
|
+
if (resolvedScene) {
|
|
206
|
+
scenePayload = resolvedScene;
|
|
207
|
+
}
|
|
208
|
+
const normalizedFrame = {
|
|
209
|
+
...frame,
|
|
210
|
+
scene: scenePayload
|
|
211
|
+
};
|
|
212
|
+
|
|
163
213
|
updatePerformanceMonitor(recordSocketFrame(performanceMonitor, frame, Date.now()));
|
|
164
|
-
engine.setAudioFrame(
|
|
214
|
+
engine.setAudioFrame(normalizedFrame);
|
|
165
215
|
frameCount += 1;
|
|
166
|
-
|
|
216
|
+
const scene = scenePayload;
|
|
217
|
+
let sceneName = String(scene?.name || currentSceneName);
|
|
218
|
+
document.body.dataset.vizcoreFrameCount = String(frameCount);
|
|
219
|
+
document.body.dataset.vizcoreScene = sceneName;
|
|
167
220
|
const now = performance.now();
|
|
168
221
|
if (
|
|
169
222
|
pendingSceneName &&
|
|
@@ -176,12 +229,8 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
176
229
|
pendingSceneName = null;
|
|
177
230
|
pendingSceneRequestedAt = 0;
|
|
178
231
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (sceneChanged) {
|
|
182
|
-
shaderParamControlsSignature = "";
|
|
183
|
-
}
|
|
184
|
-
updateShaderParamControls(frame?.scene?.layers);
|
|
232
|
+
applyRuntimeControlPresetForScene(sceneName);
|
|
233
|
+
updateSceneControls(scene);
|
|
185
234
|
const amplitude = Number(frame?.audio?.amplitude || 0).toFixed(4);
|
|
186
235
|
const bpm = Number(frame?.audio?.bpm || 0);
|
|
187
236
|
const beat = !!frame?.audio?.beat;
|
|
@@ -191,9 +240,6 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
191
240
|
}
|
|
192
241
|
const beatVisible = performance.now() < beatFlashUntil;
|
|
193
242
|
sceneStatusElement.textContent = `Scene: ${sceneName}`;
|
|
194
|
-
if (sceneChanged) {
|
|
195
|
-
renderSceneButtons();
|
|
196
|
-
}
|
|
197
243
|
frameStatusElement.textContent = `Amplitude: ${amplitude} | Frames: ${frameCount}`;
|
|
198
244
|
bpmStatusElement.textContent = `BPM: ${bpm > 0 ? bpm.toFixed(1) : "--"}`;
|
|
199
245
|
beatStatusElement.textContent = `Beat: ${beatVisible ? "ON" : "off"} | Count: ${beatCount}`;
|
|
@@ -207,6 +253,7 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
207
253
|
pendingSceneRequestedAt = 0;
|
|
208
254
|
currentSceneName = to;
|
|
209
255
|
sceneStatusElement.textContent = `Scene: ${to}`;
|
|
256
|
+
applyRuntimeControlPresetForScene(to, { force: true });
|
|
210
257
|
transitionStatusElement.textContent = `Transition: ${from} -> ${to}`;
|
|
211
258
|
renderSceneButtons();
|
|
212
259
|
},
|
|
@@ -214,11 +261,9 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
214
261
|
updateAvailableScenes(payload?.scenes);
|
|
215
262
|
const sceneName = payload?.scene?.name;
|
|
216
263
|
if (sceneName) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
shaderParamControlsSignature = "";
|
|
221
|
-
updateShaderParamControls(payload?.scene?.layers);
|
|
264
|
+
scenePayload = applyScenePayload(payload.scene);
|
|
265
|
+
applyRuntimeControlPresetForScene(sceneName, { force: true });
|
|
266
|
+
updateSceneControls(scenePayload, { forceReset: true });
|
|
222
267
|
}
|
|
223
268
|
if (Object.prototype.hasOwnProperty.call(payload || {}, "tap_tempo_key")) {
|
|
224
269
|
updateTapTempoKey(payload?.tap_tempo_key);
|
|
@@ -232,7 +277,6 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
232
277
|
}
|
|
233
278
|
if (Object.prototype.hasOwnProperty.call(payload || {}, "live_controls")) {
|
|
234
279
|
applyLiveControls({
|
|
235
|
-
...liveControls,
|
|
236
280
|
...normalizeLiveControls(payload?.live_controls),
|
|
237
281
|
});
|
|
238
282
|
}
|
|
@@ -240,12 +284,18 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
240
284
|
onLatencyProbe: (payload) => {
|
|
241
285
|
updatePerformanceMonitor(recordLatencyProbe(performanceMonitor, payload, Date.now()));
|
|
242
286
|
},
|
|
287
|
+
onRuntimeError: (payload) => {
|
|
288
|
+
updateRuntimeErrorStatus(payload);
|
|
289
|
+
},
|
|
243
290
|
onStatus: (status) => {
|
|
244
291
|
updatePerformanceMonitor(recordConnectionStatus(performanceMonitor, status));
|
|
245
292
|
if (status === "connected") {
|
|
246
293
|
lastConnectedAt = new Date();
|
|
247
294
|
startLatencyProbeLoop();
|
|
248
295
|
syncAudioTransportToServer({ force: true });
|
|
296
|
+
if (runtimeErrorStatusElement) {
|
|
297
|
+
runtimeErrorStatusElement.textContent = "Runtime: ok";
|
|
298
|
+
}
|
|
249
299
|
} else {
|
|
250
300
|
stopLatencyProbeLoop();
|
|
251
301
|
pendingSceneName = null;
|
|
@@ -253,9 +303,12 @@ const client = new WebSocketClient(websocketUrl, {
|
|
|
253
303
|
currentSceneName = "unknown";
|
|
254
304
|
sceneStatusElement.textContent = "Scene: unknown";
|
|
255
305
|
renderSceneButtons();
|
|
306
|
+
if (runtimeErrorStatusElement) {
|
|
307
|
+
runtimeErrorStatusElement.textContent = "Runtime: disconnected";
|
|
308
|
+
}
|
|
256
309
|
}
|
|
257
|
-
|
|
258
|
-
|
|
310
|
+
const connectedAt = lastConnectedAt ? ` | Last connected: ${formatClock(lastConnectedAt)}` : "";
|
|
311
|
+
wsStatusElement.textContent = `WebSocket: ${status} (${websocketUrl})${connectedAt}`;
|
|
259
312
|
}
|
|
260
313
|
});
|
|
261
314
|
|
|
@@ -298,9 +351,11 @@ function applyRuntime(runtime) {
|
|
|
298
351
|
applyRuntimeGlobals(runtime?.globals);
|
|
299
352
|
}
|
|
300
353
|
applyRuntimeControlPreset(runtime?.control_preset);
|
|
354
|
+
updatePerformanceMonitor(recordWebSocketBackpressure(performanceMonitor, runtime?.websocket_backpressure));
|
|
301
355
|
|
|
302
356
|
const fileName = runtime?.audio_file_name;
|
|
303
357
|
const fileUrl = runtime?.audio_file_url;
|
|
358
|
+
applyRuntimeAudioInputHealth(runtime?.input);
|
|
304
359
|
if (!fileUrl) {
|
|
305
360
|
engine.setMediaElement(null);
|
|
306
361
|
audioTrackStatusElement.textContent = "Track: none";
|
|
@@ -313,6 +368,33 @@ function applyRuntime(runtime) {
|
|
|
313
368
|
setupAudioPlayback(fileUrl);
|
|
314
369
|
}
|
|
315
370
|
|
|
371
|
+
function applyRuntimeAudioInputHealth(input) {
|
|
372
|
+
if (!audioHealthStatusElement) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const source = String(input?.source || "unknown");
|
|
377
|
+
const sampleRate = Number(input?.sample_rate);
|
|
378
|
+
const requestedSampleRate = Number(input?.requested_sample_rate);
|
|
379
|
+
const frameSize = Number(input?.frame_size);
|
|
380
|
+
const ringBuffer = input?.ring_buffer || {};
|
|
381
|
+
const overrun = Number(ringBuffer?.overrun_count || 0);
|
|
382
|
+
const underrun = Number(ringBuffer?.underrun_count || 0);
|
|
383
|
+
const sampleRateText = Number.isFinite(sampleRate) && sampleRate > 0
|
|
384
|
+
? `${Math.round(sampleRate)}Hz`
|
|
385
|
+
: "--";
|
|
386
|
+
const requestedSampleRateText = Number.isFinite(requestedSampleRate) &&
|
|
387
|
+
requestedSampleRate > 0 &&
|
|
388
|
+
requestedSampleRate !== sampleRate
|
|
389
|
+
? ` (${Math.round(requestedSampleRate)}Hz requested)`
|
|
390
|
+
: "";
|
|
391
|
+
const frameText = Number.isFinite(frameSize) && frameSize > 0
|
|
392
|
+
? ` | Frame ${Math.round(frameSize)}`
|
|
393
|
+
: "";
|
|
394
|
+
|
|
395
|
+
audioHealthStatusElement.textContent = `Input: ${source} | Sample ${sampleRateText}${requestedSampleRateText} | ${frameText} | Overrun ${Math.max(0, overrun)} | Underrun ${Math.max(0, underrun)}`;
|
|
396
|
+
}
|
|
397
|
+
|
|
316
398
|
function applyRuntimeGlobals(globals) {
|
|
317
399
|
engine.setRuntimeGlobals(globals);
|
|
318
400
|
}
|
|
@@ -326,28 +408,205 @@ function updateControlPresetPersistence(runtime) {
|
|
|
326
408
|
}
|
|
327
409
|
|
|
328
410
|
function applyRuntimeControlPreset(value) {
|
|
329
|
-
if (runtimeControlPresetApplied) {
|
|
411
|
+
if (!value || runtimeControlPresetApplied) {
|
|
330
412
|
return;
|
|
331
413
|
}
|
|
332
414
|
|
|
333
415
|
const preset = normalizeRuntimeControlPreset(value);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
416
|
+
const hasVisual = Boolean(preset.visualSettings);
|
|
417
|
+
const hasMidi = Boolean(preset.midiLearnBindings);
|
|
418
|
+
const hasSceneOverrides = preset.sceneOverrides && Object.keys(preset.sceneOverrides).length > 0;
|
|
419
|
+
if (!hasVisual && !hasMidi && !hasSceneOverrides) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (hasVisual) {
|
|
424
|
+
const imported = importVisualSettingsPreset(
|
|
425
|
+
{ visual_settings: preset.visualSettings },
|
|
426
|
+
{ fallback: visualSettings }
|
|
427
|
+
);
|
|
428
|
+
Object.assign(visualSettings, imported);
|
|
429
|
+
runtimeControlPresetVisualBase = cloneRuntimeValue(visualSettings);
|
|
430
|
+
saveVisualSettingsPreset(browserStorage(), visualSettings);
|
|
338
431
|
syncVisualControls();
|
|
339
432
|
engine.setVisualSettings(visualSettings);
|
|
340
433
|
renderReactivityStatus("Project preset");
|
|
341
|
-
|
|
434
|
+
} else {
|
|
435
|
+
runtimeControlPresetVisualBase = cloneRuntimeValue(visualSettings);
|
|
342
436
|
}
|
|
343
437
|
|
|
344
|
-
if (
|
|
345
|
-
midiLearnBindings =
|
|
438
|
+
if (hasMidi) {
|
|
439
|
+
midiLearnBindings = cloneRuntimeValue(preset.midiLearnBindings);
|
|
440
|
+
runtimeControlPresetMidiBase = cloneRuntimeValue(midiLearnBindings);
|
|
441
|
+
saveMidiLearnBindings(browserStorage(), midiLearnBindings);
|
|
346
442
|
renderMidiLearnStatus();
|
|
347
|
-
|
|
443
|
+
} else {
|
|
444
|
+
runtimeControlPresetMidiBase = cloneRuntimeValue(midiLearnBindings);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
runtimeControlPresetSceneOverrides = cloneRuntimeValue(preset.sceneOverrides || {});
|
|
448
|
+
runtimeControlPresetApplied = true;
|
|
449
|
+
runtimeControlPresetSceneApplied = null;
|
|
450
|
+
applyRuntimeControlPresetForScene(currentSceneName, { force: true });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function applyRuntimeControlPresetForScene(sceneName, { force = false } = {}) {
|
|
454
|
+
if (!runtimeControlPresetApplied) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const normalizedScene = normalizeSceneName(sceneName || currentSceneName);
|
|
459
|
+
if (!force && runtimeControlPresetSceneApplied === normalizedScene) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
runtimeControlPresetSceneApplied = normalizedScene;
|
|
463
|
+
|
|
464
|
+
const override = normalizeRuntimeSceneOverride(runtimeControlPresetSceneOverrides[normalizedScene]) || {};
|
|
465
|
+
const nextVisual = deriveSceneVisualSettings(runtimeControlPresetVisualBase, override.visualSettings);
|
|
466
|
+
if (nextVisual) {
|
|
467
|
+
const hasVisualChanges = hasVisualSettingsChanges(visualSettings, nextVisual);
|
|
468
|
+
if (hasVisualChanges) {
|
|
469
|
+
Object.assign(visualSettings, nextVisual);
|
|
470
|
+
syncVisualControls();
|
|
471
|
+
engine.setVisualSettings(visualSettings);
|
|
472
|
+
renderReactivityStatus("Project preset");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const nextBindings = deriveSceneMidiBindings(runtimeControlPresetMidiBase, override.midiLearnBindings);
|
|
477
|
+
if (nextBindings && hasMidiLearnBindingChanges(midiLearnBindings, nextBindings)) {
|
|
478
|
+
midiLearnBindings = nextBindings;
|
|
479
|
+
renderMidiLearnStatus();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function deriveSceneVisualSettings(baseSettings, sceneVisualSettings) {
|
|
484
|
+
const base = baseSettings && typeof baseSettings === "object" ? cloneRuntimeValue(baseSettings) : {};
|
|
485
|
+
if (sceneVisualSettings) {
|
|
486
|
+
return Object.assign(base, cloneRuntimeValue(sceneVisualSettings));
|
|
487
|
+
}
|
|
488
|
+
return Object.keys(base).length ? base : null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function deriveSceneMidiBindings(baseBindings, sceneMidiBindings) {
|
|
492
|
+
const base = baseBindings && typeof baseBindings === "object" ? cloneRuntimeValue(baseBindings) : {};
|
|
493
|
+
if (sceneMidiBindings) {
|
|
494
|
+
return Object.assign(base, cloneRuntimeValue(sceneMidiBindings));
|
|
495
|
+
}
|
|
496
|
+
return Object.keys(base).length ? base : null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function normalizeRuntimeSceneOverride(value) {
|
|
500
|
+
if (!value || typeof value !== "object") {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const input = value && typeof value === "object" ? value : {};
|
|
505
|
+
const output = {};
|
|
506
|
+
const visualSettings = objectValue(input.visualSettings) || objectValue(input.visual_settings);
|
|
507
|
+
const midiLearnBindings = objectValue(input.midiLearnBindings) || objectValue(input.midi_learn_bindings);
|
|
508
|
+
if (visualSettings) {
|
|
509
|
+
output.visualSettings = visualSettings;
|
|
510
|
+
}
|
|
511
|
+
if (midiLearnBindings) {
|
|
512
|
+
output.midiLearnBindings = midiLearnBindings;
|
|
513
|
+
}
|
|
514
|
+
return Object.keys(output).length ? output : null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function syncRuntimeControlPresetSceneVisualSetting(key, value) {
|
|
518
|
+
if (!runtimeControlPresetApplied) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const sceneName = normalizeSceneName(currentSceneName);
|
|
523
|
+
const override = runtimeControlPresetSceneOverrides[sceneName];
|
|
524
|
+
if (override && typeof override === "object") {
|
|
525
|
+
const normalizedSceneOverride = normalizeRuntimeSceneOverride(override) || {};
|
|
526
|
+
const currentOverride = cloneRuntimeValue(normalizedSceneOverride);
|
|
527
|
+
currentOverride.visualSettings ||= {};
|
|
528
|
+
currentOverride.visualSettings[key] = value;
|
|
529
|
+
runtimeControlPresetSceneOverrides[sceneName] = currentOverride;
|
|
530
|
+
applyRuntimeControlPresetForScene(sceneName, { force: true });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
runtimeControlPresetVisualBase = runtimeControlPresetVisualBase || cloneRuntimeValue(visualSettings);
|
|
535
|
+
runtimeControlPresetVisualBase[key] = value;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function syncRuntimeControlPresetMidiBindings(nextBindings) {
|
|
539
|
+
if (!runtimeControlPresetApplied) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const sceneName = normalizeSceneName(currentSceneName);
|
|
544
|
+
const normalizedBindings = objectValue(nextBindings) ? cloneRuntimeValue(nextBindings) : {};
|
|
545
|
+
const override = runtimeControlPresetSceneOverrides[sceneName];
|
|
546
|
+
if (override && typeof override === "object") {
|
|
547
|
+
const normalizedSceneOverride = normalizeRuntimeSceneOverride(override) || {};
|
|
548
|
+
const currentOverride = cloneRuntimeValue(normalizedSceneOverride);
|
|
549
|
+
currentOverride.midiLearnBindings = normalizedBindings;
|
|
550
|
+
runtimeControlPresetSceneOverrides[sceneName] = currentOverride;
|
|
551
|
+
applyRuntimeControlPresetForScene(sceneName, { force: true });
|
|
552
|
+
return;
|
|
348
553
|
}
|
|
349
554
|
|
|
350
|
-
|
|
555
|
+
runtimeControlPresetMidiBase = normalizedBindings;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function syncRuntimeControlPresetBaseWithRuntime(nextVisualSettings = visualSettings) {
|
|
559
|
+
if (!runtimeControlPresetApplied) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
runtimeControlPresetVisualBase = cloneRuntimeValue(nextVisualSettings);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function hasVisualSettingsChanges(currentValue, nextValue) {
|
|
567
|
+
return (
|
|
568
|
+
currentValue.visualGain !== nextValue.visualGain ||
|
|
569
|
+
currentValue.bassBoost !== nextValue.bassBoost ||
|
|
570
|
+
currentValue.smoothing !== nextValue.smoothing ||
|
|
571
|
+
currentValue.beatHoldMs !== nextValue.beatHoldMs ||
|
|
572
|
+
currentValue.wobbleAmount !== nextValue.wobbleAmount
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function hasMidiLearnBindingChanges(currentBindings, nextBindings) {
|
|
577
|
+
if (!nextBindings) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const currentKeys = Object.keys(currentBindings || {});
|
|
581
|
+
const nextKeys = Object.keys(nextBindings);
|
|
582
|
+
if (currentKeys.length !== nextKeys.length) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
for (const signature of nextKeys) {
|
|
586
|
+
if (!Object.prototype.hasOwnProperty.call(currentBindings, signature)) {
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
if (JSON.stringify(currentBindings[signature]) !== JSON.stringify(nextBindings[signature])) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function normalizeSceneName(sceneName) {
|
|
597
|
+
return String(sceneName || "").trim() || "unknown";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function objectValue(value) {
|
|
601
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function cloneRuntimeValue(value) {
|
|
605
|
+
if (!value || typeof value !== "object") {
|
|
606
|
+
return {};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return JSON.parse(JSON.stringify(value));
|
|
351
610
|
}
|
|
352
611
|
|
|
353
612
|
function updateAvailableScenes(sceneValues) {
|
|
@@ -369,6 +628,35 @@ function updateKeyboardMappings(mappings) {
|
|
|
369
628
|
renderSceneButtons();
|
|
370
629
|
}
|
|
371
630
|
|
|
631
|
+
function updateSceneControls(scene, { forceReset = false } = {}) {
|
|
632
|
+
const sceneLayers = scene?.layers;
|
|
633
|
+
const hasLayers = Array.isArray(sceneLayers);
|
|
634
|
+
const nextSceneName = String(scene?.name || currentSceneName);
|
|
635
|
+
|
|
636
|
+
if (currentSceneName !== nextSceneName) {
|
|
637
|
+
currentSceneName = nextSceneName;
|
|
638
|
+
sceneStatusElement.textContent = `Scene: ${currentSceneName}`;
|
|
639
|
+
renderSceneButtons();
|
|
640
|
+
forceReset = true;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (!scene) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (forceReset) {
|
|
648
|
+
shaderParamControlsSignature = "";
|
|
649
|
+
shapeEditorControlsSignature = "";
|
|
650
|
+
customShapeParamControlsSignature = "";
|
|
651
|
+
mappingTargetSelectorSignature = "";
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
updateShaderParamControls(hasLayers ? sceneLayers : null);
|
|
655
|
+
updateShapeEditorControls(hasLayers ? sceneLayers : null);
|
|
656
|
+
updateCustomShapeParamControls(hasLayers ? sceneLayers : null);
|
|
657
|
+
updateMappingTargetSelector(hasLayers ? sceneLayers : null);
|
|
658
|
+
}
|
|
659
|
+
|
|
372
660
|
function normalizeSceneNames(sceneValues) {
|
|
373
661
|
const seen = new Set();
|
|
374
662
|
const names = [];
|
|
@@ -494,7 +782,240 @@ function formatShaderParamValue(value) {
|
|
|
494
782
|
return Math.abs(numeric) >= 10 ? numeric.toFixed(1) : numeric.toFixed(2);
|
|
495
783
|
}
|
|
496
784
|
|
|
497
|
-
function
|
|
785
|
+
function updateShapeEditorControls(layers) {
|
|
786
|
+
const entries = shapeEditorEntries(layers, shapeEditorOverrides);
|
|
787
|
+
const signature = shapeEditorControlsSignatureFor(entries);
|
|
788
|
+
if (signature === shapeEditorControlsSignature) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
shapeEditorControlsSignature = signature;
|
|
793
|
+
shapeEditorOverrides = pruneShapeEditorOverrides(shapeEditorOverrides, entries);
|
|
794
|
+
engine.setShapeEditorOverrides(shapeEditorOverrides);
|
|
795
|
+
renderShapeEditorControls(entries);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function shapeEditorControlsSignatureFor(entries) {
|
|
799
|
+
return entries.map((entry) => (
|
|
800
|
+
`${entry.key}:${entry.kind}:${JSON.stringify(entry.values)}`
|
|
801
|
+
)).join("|");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function renderShapeEditorControls(entries) {
|
|
805
|
+
if (!shapeEditorControlsElement) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (!entries.length) {
|
|
810
|
+
shapeEditorControlsElement.hidden = true;
|
|
811
|
+
shapeEditorControlsElement.replaceChildren();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const title = document.createElement("p");
|
|
816
|
+
title.className = "shader-param-controls__title";
|
|
817
|
+
title.textContent = "Shape Editor";
|
|
818
|
+
const controls = entries.map((entry) => createShapeEditorControl(entry));
|
|
819
|
+
shapeEditorControlsElement.replaceChildren(title, ...controls);
|
|
820
|
+
shapeEditorControlsElement.hidden = false;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function createShapeEditorControl(entry) {
|
|
824
|
+
const section = document.createElement("details");
|
|
825
|
+
const summary = document.createElement("summary");
|
|
826
|
+
summary.textContent = entry.label;
|
|
827
|
+
section.append(summary);
|
|
828
|
+
section.append(
|
|
829
|
+
createShapeKindControl(entry),
|
|
830
|
+
createShapeNumberControl(entry, "translateX", "Move X", -640, 640, 1),
|
|
831
|
+
createShapeNumberControl(entry, "translateY", "Move Y", -360, 360, 1),
|
|
832
|
+
createShapeNumberControl(entry, "rotate", "Rotate", -180, 180, 1),
|
|
833
|
+
createShapeNumberControl(entry, "scaleX", "Scale X", -4, 4, 0.05),
|
|
834
|
+
createShapeNumberControl(entry, "scaleY", "Scale Y", -4, 4, 0.05),
|
|
835
|
+
createShapeNumberControl(entry, "opacity", "Opacity", 0, 1, 0.05),
|
|
836
|
+
createShapeColorControl(entry, "fill", "Fill"),
|
|
837
|
+
createShapeColorControl(entry, "strokeColor", "Stroke"),
|
|
838
|
+
createShapeNumberControl(entry, "strokeWidth", "Stroke W", 0, 24, 0.5)
|
|
839
|
+
);
|
|
840
|
+
return section;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function createShapeKindControl(entry) {
|
|
844
|
+
const label = document.createElement("label");
|
|
845
|
+
const name = document.createElement("span");
|
|
846
|
+
const select = document.createElement("select");
|
|
847
|
+
name.textContent = "Kind";
|
|
848
|
+
["circle", "line", "rect", "polygon", "polyline", "path", "star"].forEach((kind) => {
|
|
849
|
+
const option = document.createElement("option");
|
|
850
|
+
option.value = kind;
|
|
851
|
+
option.textContent = kind;
|
|
852
|
+
select.append(option);
|
|
853
|
+
});
|
|
854
|
+
select.value = entry.kind;
|
|
855
|
+
select.addEventListener("change", () => {
|
|
856
|
+
writeShapeEditorOverride(entry, { ...entry.values, kind: select.value });
|
|
857
|
+
});
|
|
858
|
+
label.append(name, select);
|
|
859
|
+
return label;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function createShapeNumberControl(entry, key, labelText, min, max, step) {
|
|
863
|
+
const label = document.createElement("label");
|
|
864
|
+
const name = document.createElement("span");
|
|
865
|
+
const input = document.createElement("input");
|
|
866
|
+
const value = document.createElement("output");
|
|
867
|
+
name.textContent = labelText;
|
|
868
|
+
input.type = "range";
|
|
869
|
+
input.min = String(min);
|
|
870
|
+
input.max = String(max);
|
|
871
|
+
input.step = String(step);
|
|
872
|
+
input.value = String(entry.values[key]);
|
|
873
|
+
value.value = formatShaderParamValue(entry.values[key]);
|
|
874
|
+
input.addEventListener("input", () => {
|
|
875
|
+
const numeric = Number(input.value);
|
|
876
|
+
writeShapeEditorOverride(entry, { ...entry.values, [key]: numeric });
|
|
877
|
+
value.value = formatShaderParamValue(numeric);
|
|
878
|
+
});
|
|
879
|
+
label.append(name, input, value);
|
|
880
|
+
return label;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function createShapeColorControl(entry, key, labelText) {
|
|
884
|
+
const label = document.createElement("label");
|
|
885
|
+
const name = document.createElement("span");
|
|
886
|
+
const input = document.createElement("input");
|
|
887
|
+
const value = document.createElement("output");
|
|
888
|
+
name.textContent = labelText;
|
|
889
|
+
input.type = "color";
|
|
890
|
+
input.value = entry.values[key];
|
|
891
|
+
value.value = entry.values[key];
|
|
892
|
+
input.addEventListener("input", () => {
|
|
893
|
+
writeShapeEditorOverride(entry, { ...entry.values, [key]: input.value, [`${key}Enabled`]: true });
|
|
894
|
+
value.value = input.value;
|
|
895
|
+
});
|
|
896
|
+
label.append(name, input, value);
|
|
897
|
+
return label;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function writeShapeEditorOverride(entry, values) {
|
|
901
|
+
shapeEditorOverrides[entry.layerKey] ||= {};
|
|
902
|
+
shapeEditorOverrides[entry.layerKey][entry.shapeIndex] = normalizeShapeEditorPatch(values);
|
|
903
|
+
engine.setShapeEditorOverrides(shapeEditorOverrides);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function updateCustomShapeParamControls(layers) {
|
|
907
|
+
const entries = customShapeParamControlEntries(layers, customShapeParamOverrides);
|
|
908
|
+
const signature = customShapeParamControlsSignatureFor(entries);
|
|
909
|
+
if (signature === customShapeParamControlsSignature) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
customShapeParamControlsSignature = signature;
|
|
914
|
+
customShapeParamOverrides = pruneCustomShapeParamOverrides(customShapeParamOverrides, entries);
|
|
915
|
+
renderCustomShapeParamControls(entries);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function customShapeParamControlsSignatureFor(entries) {
|
|
919
|
+
return entries.map((entry) => (
|
|
920
|
+
`${entry.key}:${entry.min}:${entry.max}:${entry.step}:${entry.value}`
|
|
921
|
+
)).join("|");
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function renderCustomShapeParamControls(entries) {
|
|
925
|
+
if (!customShapeParamControlsElement) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (!entries.length) {
|
|
930
|
+
customShapeParamControlsElement.hidden = true;
|
|
931
|
+
customShapeParamControlsElement.replaceChildren();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const title = document.createElement("p");
|
|
936
|
+
title.className = "shader-param-controls__title";
|
|
937
|
+
title.textContent = "Custom Shape Params";
|
|
938
|
+
const controls = entries.map((entry) => createCustomShapeParamControl(entry));
|
|
939
|
+
customShapeParamControlsElement.replaceChildren(title, ...controls);
|
|
940
|
+
customShapeParamControlsElement.hidden = false;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function createCustomShapeParamControl(entry) {
|
|
944
|
+
const label = document.createElement("label");
|
|
945
|
+
const name = document.createElement("span");
|
|
946
|
+
const input = document.createElement("input");
|
|
947
|
+
const value = document.createElement("output");
|
|
948
|
+
name.textContent = entry.label;
|
|
949
|
+
input.type = "range";
|
|
950
|
+
input.min = String(entry.min);
|
|
951
|
+
input.max = String(entry.max);
|
|
952
|
+
input.step = String(entry.step);
|
|
953
|
+
input.value = String(entry.value);
|
|
954
|
+
value.value = formatShaderParamValue(entry.value);
|
|
955
|
+
input.addEventListener("input", () => {
|
|
956
|
+
const numeric = Number(input.value);
|
|
957
|
+
customShapeParamOverrides[entry.layerKey] ||= {};
|
|
958
|
+
customShapeParamOverrides[entry.layerKey][entry.customShapeIndex] ||= {};
|
|
959
|
+
customShapeParamOverrides[entry.layerKey][entry.customShapeIndex][entry.paramName] = numeric;
|
|
960
|
+
client.send("custom_shape_param", customShapeParamMessage(entry, numeric));
|
|
961
|
+
value.value = formatShaderParamValue(numeric);
|
|
962
|
+
});
|
|
963
|
+
label.append(name, input, value);
|
|
964
|
+
return label;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function updateMappingTargetSelector(layers) {
|
|
968
|
+
const options = mappingTargetOptions(layers);
|
|
969
|
+
const signature = mappingTargetSignature(options);
|
|
970
|
+
if (signature === mappingTargetSelectorSignature) {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
mappingTargetSelectorSignature = signature;
|
|
975
|
+
renderMappingTargetSelector(options);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function renderMappingTargetSelector(options) {
|
|
979
|
+
if (!mappingTargetSelectorElement) {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (!options.length) {
|
|
984
|
+
selectedMappingTarget = "";
|
|
985
|
+
mappingTargetSelectorElement.hidden = true;
|
|
986
|
+
mappingTargetSelectorElement.replaceChildren();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const title = document.createElement("p");
|
|
991
|
+
title.className = "shader-param-controls__title";
|
|
992
|
+
title.textContent = "Mapping Targets";
|
|
993
|
+
const label = document.createElement("label");
|
|
994
|
+
const name = document.createElement("span");
|
|
995
|
+
const select = document.createElement("select");
|
|
996
|
+
const output = document.createElement("output");
|
|
997
|
+
name.textContent = "Target";
|
|
998
|
+
options.forEach((option) => {
|
|
999
|
+
const item = document.createElement("option");
|
|
1000
|
+
item.value = option.target;
|
|
1001
|
+
item.textContent = option.label;
|
|
1002
|
+
select.append(item);
|
|
1003
|
+
});
|
|
1004
|
+
if (!options.some((option) => option.target === selectedMappingTarget)) {
|
|
1005
|
+
selectedMappingTarget = options[0].target;
|
|
1006
|
+
}
|
|
1007
|
+
select.value = selectedMappingTarget;
|
|
1008
|
+
output.value = selectedMappingTarget;
|
|
1009
|
+
select.addEventListener("change", () => {
|
|
1010
|
+
selectedMappingTarget = select.value;
|
|
1011
|
+
output.value = select.value;
|
|
1012
|
+
});
|
|
1013
|
+
label.append(name, select, output);
|
|
1014
|
+
mappingTargetSelectorElement.replaceChildren(title, label);
|
|
1015
|
+
mappingTargetSelectorElement.hidden = false;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function requestSceneSwitch(sceneName, effect = null) {
|
|
498
1019
|
if (!sceneName || sceneName === currentSceneName) {
|
|
499
1020
|
return;
|
|
500
1021
|
}
|
|
@@ -504,17 +1025,29 @@ function requestSceneSwitch(sceneName) {
|
|
|
504
1025
|
currentSceneName = sceneName;
|
|
505
1026
|
sceneStatusElement.textContent = `Scene: ${sceneName}`;
|
|
506
1027
|
renderSceneButtons();
|
|
507
|
-
|
|
1028
|
+
const payload = { scene: sceneName };
|
|
1029
|
+
const normalizedEffect = normalizeTransitionEffect(effect);
|
|
1030
|
+
if (normalizedEffect) {
|
|
1031
|
+
payload.effect = normalizedEffect;
|
|
1032
|
+
}
|
|
1033
|
+
client.send("switch_scene", payload);
|
|
508
1034
|
}
|
|
509
1035
|
|
|
510
1036
|
function applyKeyboardAction(action) {
|
|
511
1037
|
if (action?.type === "switch_scene") {
|
|
512
|
-
requestSceneSwitch(action.scene);
|
|
1038
|
+
requestSceneSwitch(action.scene, action.effect);
|
|
513
1039
|
return;
|
|
514
1040
|
}
|
|
515
1041
|
|
|
516
1042
|
if (action?.type === "live_control") {
|
|
517
|
-
|
|
1043
|
+
if (Object.prototype.hasOwnProperty.call(action, "value")) {
|
|
1044
|
+
applyLiveControls({
|
|
1045
|
+
[action?.control]: normalizeLiveControlPayload(action),
|
|
1046
|
+
});
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
applyLiveControls(toggleLiveControl(liveControls, action?.control));
|
|
518
1051
|
}
|
|
519
1052
|
}
|
|
520
1053
|
|
|
@@ -546,6 +1079,20 @@ function sendLatencyProbe() {
|
|
|
546
1079
|
});
|
|
547
1080
|
}
|
|
548
1081
|
|
|
1082
|
+
function normalizeTransitionEffect(effect) {
|
|
1083
|
+
if (!effect) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
if (typeof effect === "string" || typeof effect === "number" || typeof effect === "symbol") {
|
|
1087
|
+
return String(effect);
|
|
1088
|
+
}
|
|
1089
|
+
if (typeof effect !== "object" || Array.isArray(effect)) {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return effect;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
549
1096
|
function updatePerformanceMonitor(nextState) {
|
|
550
1097
|
Object.assign(performanceMonitor, nextState);
|
|
551
1098
|
renderPerformanceMonitor();
|
|
@@ -565,6 +1112,15 @@ function bindShaderCompileMetrics() {
|
|
|
565
1112
|
});
|
|
566
1113
|
}
|
|
567
1114
|
|
|
1115
|
+
function bindRendererMetrics() {
|
|
1116
|
+
window.addEventListener(RENDERER_CAPABILITIES_EVENT, (event) => {
|
|
1117
|
+
updatePerformanceMonitor(recordRendererCapabilities(performanceMonitor, event.detail));
|
|
1118
|
+
});
|
|
1119
|
+
window.addEventListener(RENDERER_SAFE_MODE_EVENT, (event) => {
|
|
1120
|
+
updatePerformanceMonitor(recordRendererSafeMode(performanceMonitor, event.detail));
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
568
1124
|
function bindLiveControls() {
|
|
569
1125
|
bindLiveControlButton(blackoutButton, "blackout");
|
|
570
1126
|
bindLiveControlButton(freezeButton, "freeze");
|
|
@@ -579,7 +1135,7 @@ function bindLiveControls() {
|
|
|
579
1135
|
const action = shortcutActionForKey(event);
|
|
580
1136
|
if (action) {
|
|
581
1137
|
event.preventDefault();
|
|
582
|
-
|
|
1138
|
+
applyKeyboardAction({ type: "live_control", control: action });
|
|
583
1139
|
return;
|
|
584
1140
|
}
|
|
585
1141
|
|
|
@@ -613,16 +1169,54 @@ function bindLiveControlButton(button, control) {
|
|
|
613
1169
|
}
|
|
614
1170
|
|
|
615
1171
|
function applyLiveControls(nextState) {
|
|
616
|
-
|
|
1172
|
+
if (!nextState || typeof nextState !== "object") {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const nextBlackout = mergeLiveControlState(
|
|
1177
|
+
liveControls.blackout,
|
|
1178
|
+
nextState.blackout
|
|
1179
|
+
);
|
|
1180
|
+
const nextFreeze = mergeLiveControlState(
|
|
1181
|
+
liveControls.freeze,
|
|
1182
|
+
nextState.freeze
|
|
1183
|
+
);
|
|
1184
|
+
Object.assign(liveControls, {
|
|
1185
|
+
blackout: nextBlackout,
|
|
1186
|
+
freeze: nextFreeze,
|
|
1187
|
+
});
|
|
617
1188
|
engine.setLiveControls(liveControls);
|
|
618
1189
|
renderLiveControlStatus();
|
|
619
1190
|
}
|
|
620
1191
|
|
|
1192
|
+
function mergeLiveControlState(currentState, nextState) {
|
|
1193
|
+
const current = normalizeLiveControlPayload(currentState);
|
|
1194
|
+
if (nextState === undefined) {
|
|
1195
|
+
return current;
|
|
1196
|
+
}
|
|
1197
|
+
if (!nextState || typeof nextState !== "object" || Array.isArray(nextState)) {
|
|
1198
|
+
return normalizeLiveControlPayload(nextState);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const normalized = normalizeLiveControlPayload(nextState);
|
|
1202
|
+
const nextHasFade = Object.prototype.hasOwnProperty.call(nextState, "fade");
|
|
1203
|
+
const nextHasRelease = Object.prototype.hasOwnProperty.call(nextState, "release");
|
|
1204
|
+
const nextHasColor = Object.prototype.hasOwnProperty.call(nextState, "color");
|
|
1205
|
+
|
|
1206
|
+
return {
|
|
1207
|
+
...current,
|
|
1208
|
+
...normalized,
|
|
1209
|
+
...(nextHasFade ? { fade: normalized.fade } : {}),
|
|
1210
|
+
...(nextHasRelease ? { release: normalized.release } : {}),
|
|
1211
|
+
...(nextHasColor ? { color: normalized.color } : {}),
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
621
1215
|
function normalizeLiveControls(value) {
|
|
622
1216
|
const input = value && typeof value === "object" ? value : {};
|
|
623
1217
|
return {
|
|
624
|
-
blackout:
|
|
625
|
-
freeze:
|
|
1218
|
+
blackout: normalizeLiveControlPayload(input.blackout),
|
|
1219
|
+
freeze: normalizeLiveControlPayload(input.freeze),
|
|
626
1220
|
};
|
|
627
1221
|
}
|
|
628
1222
|
|
|
@@ -632,13 +1226,15 @@ function renderLiveControlStatus() {
|
|
|
632
1226
|
}
|
|
633
1227
|
|
|
634
1228
|
if (blackoutButton) {
|
|
635
|
-
|
|
636
|
-
blackoutButton.
|
|
1229
|
+
const isActive = isLiveControlEnabled(liveControls.blackout);
|
|
1230
|
+
blackoutButton.classList.toggle("is-active", isActive);
|
|
1231
|
+
blackoutButton.setAttribute("aria-pressed", String(isActive));
|
|
637
1232
|
}
|
|
638
1233
|
|
|
639
1234
|
if (freezeButton) {
|
|
640
|
-
|
|
641
|
-
freezeButton.
|
|
1235
|
+
const isActive = isLiveControlEnabled(liveControls.freeze);
|
|
1236
|
+
freezeButton.classList.toggle("is-active", isActive);
|
|
1237
|
+
freezeButton.setAttribute("aria-pressed", String(isActive));
|
|
642
1238
|
}
|
|
643
1239
|
}
|
|
644
1240
|
|
|
@@ -747,6 +1343,7 @@ function bindVisualControl(control, key, parser = Number) {
|
|
|
747
1343
|
visualSettings[key] = parser(control.value);
|
|
748
1344
|
engine.setVisualSettings(visualSettings);
|
|
749
1345
|
renderReactivityStatus();
|
|
1346
|
+
syncRuntimeControlPresetSceneVisualSetting(key, visualSettings[key]);
|
|
750
1347
|
});
|
|
751
1348
|
}
|
|
752
1349
|
|
|
@@ -754,6 +1351,7 @@ function bindVisualPresetControls() {
|
|
|
754
1351
|
if (reactivitySaveButton) {
|
|
755
1352
|
reactivitySaveButton.addEventListener("click", () => {
|
|
756
1353
|
Object.assign(visualSettings, saveVisualSettingsPreset(browserStorage(), visualSettings));
|
|
1354
|
+
syncRuntimeControlPresetBaseWithRuntime(visualSettings);
|
|
757
1355
|
renderReactivityStatus("Saved");
|
|
758
1356
|
});
|
|
759
1357
|
}
|
|
@@ -763,6 +1361,7 @@ function bindVisualPresetControls() {
|
|
|
763
1361
|
Object.assign(visualSettings, loadVisualSettingsPreset(browserStorage(), { fallback: visualSettings }));
|
|
764
1362
|
syncVisualControls();
|
|
765
1363
|
engine.setVisualSettings(visualSettings);
|
|
1364
|
+
syncRuntimeControlPresetBaseWithRuntime(visualSettings);
|
|
766
1365
|
renderReactivityStatus("Loaded");
|
|
767
1366
|
});
|
|
768
1367
|
}
|
|
@@ -807,6 +1406,7 @@ function bindVisualPresetControls() {
|
|
|
807
1406
|
Object.assign(visualSettings, saveVisualSettingsPreset(browserStorage(), visualSettings));
|
|
808
1407
|
syncVisualControls();
|
|
809
1408
|
engine.setVisualSettings(visualSettings);
|
|
1409
|
+
syncRuntimeControlPresetBaseWithRuntime(visualSettings);
|
|
810
1410
|
renderReactivityStatus("Imported");
|
|
811
1411
|
});
|
|
812
1412
|
}
|
|
@@ -820,6 +1420,7 @@ async function saveProjectControlPreset() {
|
|
|
820
1420
|
body: JSON.stringify({
|
|
821
1421
|
visual_settings: visualSettings,
|
|
822
1422
|
midi_learn_bindings: midiLearnBindings,
|
|
1423
|
+
scene_overrides: runtimeControlPresetSceneOverrides,
|
|
823
1424
|
}),
|
|
824
1425
|
});
|
|
825
1426
|
return response.ok;
|
|
@@ -904,6 +1505,7 @@ function handleMidiMessage(event) {
|
|
|
904
1505
|
if (pendingMidiLearnAction && midiMessageActive(event?.data)) {
|
|
905
1506
|
midiLearnBindings = upsertMidiLearnBinding(midiLearnBindings, signature, pendingMidiLearnAction);
|
|
906
1507
|
midiLearnBindings = saveMidiLearnBindings(browserStorage(), midiLearnBindings);
|
|
1508
|
+
syncRuntimeControlPresetMidiBindings(midiLearnBindings);
|
|
907
1509
|
renderMidiLearnStatus(`Learned ${midiSignatureLabel(signature)} -> ${midiLearnActionLabel(pendingMidiLearnAction)}`);
|
|
908
1510
|
pendingMidiLearnAction = null;
|
|
909
1511
|
return;
|
|
@@ -922,6 +1524,7 @@ function applyMidiLearnAction(action, unitValue, active) {
|
|
|
922
1524
|
visualSettings[action.key] = visualSettingFromUnit(action.key, unitValue, visualSettings[action.key]);
|
|
923
1525
|
syncVisualControls();
|
|
924
1526
|
engine.setVisualSettings(visualSettings);
|
|
1527
|
+
syncRuntimeControlPresetSceneVisualSetting(action.key, visualSettings[action.key]);
|
|
925
1528
|
renderReactivityStatus("MIDI");
|
|
926
1529
|
return;
|
|
927
1530
|
}
|
|
@@ -931,13 +1534,19 @@ function applyMidiLearnAction(action, unitValue, active) {
|
|
|
931
1534
|
}
|
|
932
1535
|
|
|
933
1536
|
if (action.type === "switch_scene") {
|
|
934
|
-
requestSceneSwitch(action.scene);
|
|
1537
|
+
requestSceneSwitch(action.scene, action.effect);
|
|
935
1538
|
renderMidiLearnStatus(`MIDI: ${midiLearnActionLabel(action)}`);
|
|
936
1539
|
return;
|
|
937
1540
|
}
|
|
938
1541
|
|
|
939
1542
|
if (action.type === "live_control") {
|
|
940
|
-
|
|
1543
|
+
if (Object.prototype.hasOwnProperty.call(action, "value")) {
|
|
1544
|
+
applyLiveControls({
|
|
1545
|
+
[action.control]: normalizeLiveControlPayload(action),
|
|
1546
|
+
});
|
|
1547
|
+
} else {
|
|
1548
|
+
applyLiveControls(toggleLiveControl(liveControls, action.control));
|
|
1549
|
+
}
|
|
941
1550
|
renderMidiLearnStatus(`MIDI: ${midiLearnActionLabel(action)}`);
|
|
942
1551
|
}
|
|
943
1552
|
}
|
|
@@ -1006,6 +1615,7 @@ function renderMidiLearnStatus(prefix = null) {
|
|
|
1006
1615
|
function bindShaderErrorOverlay() {
|
|
1007
1616
|
window.addEventListener(SHADER_ERROR_EVENT, (event) => {
|
|
1008
1617
|
renderShaderError(event.detail);
|
|
1618
|
+
reportShaderErrorToServer(event.detail);
|
|
1009
1619
|
});
|
|
1010
1620
|
if (shaderErrorCloseButton) {
|
|
1011
1621
|
shaderErrorCloseButton.addEventListener("click", () => {
|
|
@@ -1026,6 +1636,31 @@ function renderShaderError(detail) {
|
|
|
1026
1636
|
shaderErrorOverlay.hidden = false;
|
|
1027
1637
|
}
|
|
1028
1638
|
|
|
1639
|
+
function reportShaderErrorToServer(detail = {}) {
|
|
1640
|
+
const source = String(detail?.source || "shader").trim() || "shader";
|
|
1641
|
+
const layer = String(detail?.name || "layer").trim();
|
|
1642
|
+
const shader = String(detail?.shader || "unknown").trim();
|
|
1643
|
+
const event = String(detail?.event || "shader_failed").trim() || "shader_failed";
|
|
1644
|
+
const phase = String(detail?.phase || "").trim();
|
|
1645
|
+
const message = String(detail?.message || "").trim();
|
|
1646
|
+
const fullMessage = `${layer} (${shader}) ${phase ? `[${phase}] ` : ""}${message}`;
|
|
1647
|
+
client.send("client_runtime_error", {
|
|
1648
|
+
source,
|
|
1649
|
+
event,
|
|
1650
|
+
context: "shader compile failed",
|
|
1651
|
+
message: fullMessage,
|
|
1652
|
+
layer,
|
|
1653
|
+
shader,
|
|
1654
|
+
phase
|
|
1655
|
+
});
|
|
1656
|
+
updateRuntimeErrorStatus({
|
|
1657
|
+
source,
|
|
1658
|
+
event,
|
|
1659
|
+
context: "shader compile failed",
|
|
1660
|
+
message: fullMessage
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1029
1664
|
function initializeFftPreview(container) {
|
|
1030
1665
|
if (!container) {
|
|
1031
1666
|
return [];
|
|
@@ -1065,6 +1700,26 @@ function renderAudioInspector(audio) {
|
|
|
1065
1700
|
}
|
|
1066
1701
|
}
|
|
1067
1702
|
|
|
1703
|
+
function updateRuntimeErrorStatus(payload = {}) {
|
|
1704
|
+
if (!runtimeErrorStatusElement) {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
const source = String(payload?.source || "runtime").trim();
|
|
1709
|
+
const event = String(payload?.event || "").trim();
|
|
1710
|
+
const context = String(payload?.context || "runtime error").trim();
|
|
1711
|
+
const message = String(payload?.message || "").trim();
|
|
1712
|
+
const frameId = payload?.frame_id;
|
|
1713
|
+
|
|
1714
|
+
const detail = [context, event].filter(Boolean).join(" / ");
|
|
1715
|
+
const frameText = Number.isFinite(frameId) ? ` (frame ${frameId})` : "";
|
|
1716
|
+
const text = message
|
|
1717
|
+
? `Runtime (${source}): ${detail}${frameText} | ${message}`
|
|
1718
|
+
: `Runtime (${source}): ${detail}${frameText}`;
|
|
1719
|
+
|
|
1720
|
+
runtimeErrorStatusElement.textContent = text;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1068
1723
|
function setMeter(fill, valueElement, value, digits) {
|
|
1069
1724
|
if (fill) {
|
|
1070
1725
|
fill.style.setProperty("--meter-value", value.toFixed(4));
|
|
@@ -1076,5 +1731,8 @@ function setMeter(fill, valueElement, value, digits) {
|
|
|
1076
1731
|
|
|
1077
1732
|
function buildWebSocketUrl() {
|
|
1078
1733
|
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
|
1079
|
-
|
|
1734
|
+
const mode = new URLSearchParams(window.location.search || "").get("mode");
|
|
1735
|
+
const normalizedMode = String(mode || "").toLowerCase();
|
|
1736
|
+
const role = projectorMode || normalizedMode === "projector" ? "projector" : normalizedMode === "monitor" ? "monitor" : "control";
|
|
1737
|
+
return `${protocol}://${window.location.host}/ws?role=${encodeURIComponent(role)}`;
|
|
1080
1738
|
}
|