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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { LayerManager } from "./layer-manager.js";
|
|
2
2
|
import { ShaderManager } from "./shader-manager.js";
|
|
3
3
|
import { applyShaderParamOverrides } from "../shader-param-controls.js";
|
|
4
|
+
import { applyShapeEditorOverrides } from "../shape-editor-controls.js";
|
|
5
|
+
|
|
6
|
+
export const RENDERER_CAPABILITIES_EVENT = "vizcore:renderer-capabilities";
|
|
7
|
+
export const RENDERER_SAFE_MODE_EVENT = "vizcore:renderer-safe-mode";
|
|
4
8
|
|
|
5
9
|
export class Engine {
|
|
6
10
|
constructor(canvas) {
|
|
@@ -13,25 +17,54 @@ export class Engine {
|
|
|
13
17
|
this.currentRotationSpeed = 0.5;
|
|
14
18
|
this.mediaElement = null;
|
|
15
19
|
this.lastMediaTime = null;
|
|
20
|
+
this.resizeHandler = null;
|
|
21
|
+
this.rendererCapabilities = null;
|
|
22
|
+
this.safeModeState = {
|
|
23
|
+
active: false,
|
|
24
|
+
slowFrames: 0,
|
|
25
|
+
fastFrames: 0,
|
|
26
|
+
};
|
|
16
27
|
this.visualSettings = {
|
|
17
28
|
visualGain: 1,
|
|
18
29
|
bassBoost: 1,
|
|
19
30
|
smoothing: 0,
|
|
20
31
|
beatHoldMs: 180,
|
|
21
32
|
wobbleAmount: 1,
|
|
33
|
+
maxDevicePixelRatio: 2,
|
|
34
|
+
safeMode: true,
|
|
35
|
+
safeModeFrameMs: 34,
|
|
36
|
+
safeModeScale: 0.75,
|
|
22
37
|
};
|
|
23
38
|
this.visualAudioState = null;
|
|
24
39
|
this.liveControls = {
|
|
25
|
-
blackout: false,
|
|
26
|
-
freeze: false,
|
|
40
|
+
blackout: { enabled: false, color: [0, 0, 0, 1] },
|
|
41
|
+
freeze: { enabled: false },
|
|
42
|
+
};
|
|
43
|
+
this.liveControlRuntime = {
|
|
44
|
+
blackout: {
|
|
45
|
+
opacity: 0,
|
|
46
|
+
targetOpacity: 0,
|
|
47
|
+
transitionFrom: 0,
|
|
48
|
+
transitionStartedAt: 0,
|
|
49
|
+
transitionDuration: 0,
|
|
50
|
+
color: [0, 0, 0, 1],
|
|
51
|
+
},
|
|
52
|
+
freeze: {
|
|
53
|
+
enabled: false,
|
|
54
|
+
targetEnabled: false,
|
|
55
|
+
transitionStartedAt: 0,
|
|
56
|
+
transitionDuration: 0,
|
|
57
|
+
},
|
|
27
58
|
};
|
|
28
59
|
this.runtimeGlobals = {};
|
|
29
60
|
this.shaderParamOverrides = {};
|
|
61
|
+
this.shapeEditorOverrides = {};
|
|
30
62
|
this.beatHoldUntil = 0;
|
|
31
63
|
this.frame = {
|
|
32
64
|
audio: {
|
|
33
65
|
amplitude: 0,
|
|
34
66
|
bands: { sub: 0, low: 0, mid: 0, high: 0 },
|
|
67
|
+
band_peaks: { sub: 0, low: 0, mid: 0, high: 0 },
|
|
35
68
|
fft: [],
|
|
36
69
|
onset: 0,
|
|
37
70
|
onsets: { sub: 0, low: 0, mid: 0, high: 0 },
|
|
@@ -39,6 +72,14 @@ export class Engine {
|
|
|
39
72
|
beat: false,
|
|
40
73
|
beat_pulse: 0,
|
|
41
74
|
beat_count: 0,
|
|
75
|
+
beat_phase: 0,
|
|
76
|
+
beat_2: false,
|
|
77
|
+
beat_4: false,
|
|
78
|
+
beat_8: false,
|
|
79
|
+
beat_triplet: false,
|
|
80
|
+
bar_phase: 0,
|
|
81
|
+
bar_count: 0,
|
|
82
|
+
phrase_count: 0,
|
|
42
83
|
bpm: 0
|
|
43
84
|
},
|
|
44
85
|
scene: {
|
|
@@ -56,12 +97,18 @@ export class Engine {
|
|
|
56
97
|
|
|
57
98
|
this.shaderManager = new ShaderManager(this.gl);
|
|
58
99
|
this.layerManager = new LayerManager(this.gl, this.shaderManager);
|
|
100
|
+
this.rendererCapabilities = collectRendererCapabilities(this.gl, {
|
|
101
|
+
devicePixelRatio: currentDevicePixelRatio(),
|
|
102
|
+
effectiveDevicePixelRatio: this.effectiveDevicePixelRatio(),
|
|
103
|
+
});
|
|
104
|
+
dispatchRendererEvent(RENDERER_CAPABILITIES_EVENT, this.rendererCapabilities);
|
|
59
105
|
|
|
60
106
|
this.gl.enable(this.gl.DEPTH_TEST);
|
|
61
107
|
this.gl.enable(this.gl.BLEND);
|
|
62
108
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
63
109
|
this.resize();
|
|
64
|
-
|
|
110
|
+
this.resizeHandler = () => this.resize();
|
|
111
|
+
window.addEventListener("resize", this.resizeHandler);
|
|
65
112
|
}
|
|
66
113
|
|
|
67
114
|
setAudioFrame(frame) {
|
|
@@ -81,13 +128,108 @@ export class Engine {
|
|
|
81
128
|
...this.visualSettings,
|
|
82
129
|
...settings,
|
|
83
130
|
};
|
|
131
|
+
if (this.gl) {
|
|
132
|
+
this.resize();
|
|
133
|
+
}
|
|
84
134
|
}
|
|
85
135
|
|
|
86
136
|
setLiveControls(controls = {}) {
|
|
137
|
+
const now = performance.now();
|
|
87
138
|
this.liveControls = {
|
|
88
|
-
blackout:
|
|
89
|
-
freeze:
|
|
139
|
+
blackout: normalizeLiveControlState(controls?.blackout),
|
|
140
|
+
freeze: normalizeLiveControlState(controls?.freeze),
|
|
90
141
|
};
|
|
142
|
+
this.applyBlackoutRuntime(this.liveControls.blackout, now);
|
|
143
|
+
this.applyFreezeRuntime(this.liveControls.freeze, now);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
applyBlackoutRuntime(control, now) {
|
|
147
|
+
const targetOpacity = control.enabled ? 1 : 0;
|
|
148
|
+
const color = normalizeLiveControlColor(control.color);
|
|
149
|
+
if (color) {
|
|
150
|
+
this.liveControlRuntime.blackout.color = color;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (targetOpacity === this.liveControlRuntime.blackout.targetOpacity) {
|
|
154
|
+
this.liveControlRuntime.blackout.transitionDuration = 0;
|
|
155
|
+
this.liveControlRuntime.blackout.opacity = targetOpacity;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const transitionDuration = control.enabled
|
|
160
|
+
? finiteFloat(control.fade)
|
|
161
|
+
: finiteFloat(control.release);
|
|
162
|
+
if (!transitionDuration) {
|
|
163
|
+
this.liveControlRuntime.blackout.opacity = targetOpacity;
|
|
164
|
+
this.liveControlRuntime.blackout.targetOpacity = targetOpacity;
|
|
165
|
+
this.liveControlRuntime.blackout.transitionDuration = 0;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.liveControlRuntime.blackout.targetOpacity = targetOpacity;
|
|
170
|
+
this.liveControlRuntime.blackout.transitionFrom = this.liveControlRuntime.blackout.opacity;
|
|
171
|
+
this.liveControlRuntime.blackout.transitionStartedAt = now;
|
|
172
|
+
this.liveControlRuntime.blackout.transitionDuration = transitionDuration;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
applyFreezeRuntime(control, now) {
|
|
176
|
+
const targetEnabled = !!control.enabled;
|
|
177
|
+
if (targetEnabled === this.liveControlRuntime.freeze.targetEnabled) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const transitionDuration = targetEnabled
|
|
182
|
+
? finiteFloat(control.fade)
|
|
183
|
+
: finiteFloat(control.release);
|
|
184
|
+
if (!transitionDuration) {
|
|
185
|
+
this.liveControlRuntime.freeze.enabled = targetEnabled;
|
|
186
|
+
this.liveControlRuntime.freeze.targetEnabled = targetEnabled;
|
|
187
|
+
this.liveControlRuntime.freeze.transitionDuration = 0;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.liveControlRuntime.freeze.targetEnabled = targetEnabled;
|
|
192
|
+
this.liveControlRuntime.freeze.transitionStartedAt = now;
|
|
193
|
+
this.liveControlRuntime.freeze.transitionDuration = transitionDuration;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
updateLiveControlRuntime(time) {
|
|
197
|
+
this.updateBlackoutRuntime(time);
|
|
198
|
+
this.updateFreezeRuntime(time);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
updateBlackoutRuntime(time) {
|
|
202
|
+
const blackout = this.liveControlRuntime.blackout;
|
|
203
|
+
const duration = blackout.transitionDuration;
|
|
204
|
+
if (!duration || duration <= 0) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const elapsed = time - blackout.transitionStartedAt;
|
|
209
|
+
if (elapsed >= duration) {
|
|
210
|
+
blackout.opacity = blackout.targetOpacity;
|
|
211
|
+
blackout.transitionDuration = 0;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const progress = clamp(elapsed / duration, 0, 1);
|
|
216
|
+
blackout.opacity = blackout.transitionFrom + (blackout.targetOpacity - blackout.transitionFrom) * progress;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
updateFreezeRuntime(time) {
|
|
220
|
+
const freeze = this.liveControlRuntime.freeze;
|
|
221
|
+
const duration = freeze.transitionDuration;
|
|
222
|
+
if (!duration || duration <= 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const elapsed = time - freeze.transitionStartedAt;
|
|
227
|
+
if (elapsed < duration) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
freeze.enabled = freeze.targetEnabled;
|
|
232
|
+
freeze.transitionDuration = 0;
|
|
91
233
|
}
|
|
92
234
|
|
|
93
235
|
setRuntimeGlobals(globals = {}) {
|
|
@@ -98,14 +240,19 @@ export class Engine {
|
|
|
98
240
|
this.shaderParamOverrides = overrides && typeof overrides === "object" ? overrides : {};
|
|
99
241
|
}
|
|
100
242
|
|
|
243
|
+
setShapeEditorOverrides(overrides = {}) {
|
|
244
|
+
this.shapeEditorOverrides = overrides && typeof overrides === "object" ? overrides : {};
|
|
245
|
+
}
|
|
246
|
+
|
|
101
247
|
start() {
|
|
102
248
|
this.lastTime = performance.now();
|
|
103
249
|
requestAnimationFrame((time) => this.render(time));
|
|
104
250
|
}
|
|
105
251
|
|
|
106
252
|
resize() {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
253
|
+
const dpr = this.effectiveDevicePixelRatio();
|
|
254
|
+
const width = Math.max(1, Math.floor(this.canvas.clientWidth * dpr));
|
|
255
|
+
const height = Math.max(1, Math.floor(this.canvas.clientHeight * dpr));
|
|
109
256
|
if (this.canvas.width === width && this.canvas.height === height) {
|
|
110
257
|
return;
|
|
111
258
|
}
|
|
@@ -114,6 +261,15 @@ export class Engine {
|
|
|
114
261
|
this.gl.viewport(0, 0, width, height);
|
|
115
262
|
}
|
|
116
263
|
|
|
264
|
+
effectiveDevicePixelRatio() {
|
|
265
|
+
return resolveEffectiveDevicePixelRatio({
|
|
266
|
+
devicePixelRatio: currentDevicePixelRatio(),
|
|
267
|
+
maxDevicePixelRatio: this.visualSettings.maxDevicePixelRatio,
|
|
268
|
+
safeModeActive: this.safeModeState.active,
|
|
269
|
+
safeModeScale: this.visualSettings.safeModeScale,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
117
273
|
render(time) {
|
|
118
274
|
let deltaSeconds = (time - this.lastTime) / 1000;
|
|
119
275
|
this.lastTime = time;
|
|
@@ -136,14 +292,21 @@ export class Engine {
|
|
|
136
292
|
this.lastMediaTime = null;
|
|
137
293
|
}
|
|
138
294
|
|
|
139
|
-
|
|
140
|
-
|
|
295
|
+
this.updateSafeMode(deltaSeconds * 1000);
|
|
296
|
+
this.updateLiveControlRuntime(time);
|
|
297
|
+
|
|
298
|
+
const blackout = this.liveControlRuntime.blackout;
|
|
299
|
+
const blackoutColor = blackout.color || [0, 0, 0, 1];
|
|
300
|
+
const [blackoutRed, blackoutGreen, blackoutBlue, blackoutAlpha = 1] = blackoutColor;
|
|
301
|
+
const effectiveBlackoutAlpha = clamp(blackout.opacity * blackoutAlpha, 0, 1);
|
|
302
|
+
if (effectiveBlackoutAlpha >= 1) {
|
|
303
|
+
this.gl.clearColor(blackoutRed, blackoutGreen, blackoutBlue, 1);
|
|
141
304
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
142
305
|
requestAnimationFrame((nextTime) => this.render(nextTime));
|
|
143
306
|
return;
|
|
144
307
|
}
|
|
145
308
|
|
|
146
|
-
if (this.
|
|
309
|
+
if (this.liveControlRuntime.freeze.enabled) {
|
|
147
310
|
requestAnimationFrame((nextTime) => this.render(nextTime));
|
|
148
311
|
return;
|
|
149
312
|
}
|
|
@@ -156,7 +319,10 @@ export class Engine {
|
|
|
156
319
|
});
|
|
157
320
|
this.visualAudioState = audio;
|
|
158
321
|
const rawLayers = Array.isArray(this.frame?.scene?.layers) ? this.frame.scene.layers : [];
|
|
159
|
-
const layers =
|
|
322
|
+
const layers = applyShapeEditorOverrides(
|
|
323
|
+
applyShaderParamOverrides(rawLayers, this.shaderParamOverrides),
|
|
324
|
+
this.shapeEditorOverrides
|
|
325
|
+
);
|
|
160
326
|
const amplitude = clamp(Number(audio.amplitude || 0), 0, 1);
|
|
161
327
|
const rotationSpeed = resolveRotationSpeed(layers, amplitude);
|
|
162
328
|
this.currentRotationSpeed += (rotationSpeed - this.currentRotationSpeed) * 0.1;
|
|
@@ -170,6 +336,16 @@ export class Engine {
|
|
|
170
336
|
);
|
|
171
337
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
172
338
|
|
|
339
|
+
if (effectiveBlackoutAlpha > 0) {
|
|
340
|
+
this.gl.clearColor(
|
|
341
|
+
blackoutRed,
|
|
342
|
+
blackoutGreen,
|
|
343
|
+
blackoutBlue,
|
|
344
|
+
effectiveBlackoutAlpha
|
|
345
|
+
);
|
|
346
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
347
|
+
}
|
|
348
|
+
|
|
173
349
|
this.layerManager.renderScene({
|
|
174
350
|
layers,
|
|
175
351
|
audio,
|
|
@@ -182,8 +358,115 @@ export class Engine {
|
|
|
182
358
|
|
|
183
359
|
requestAnimationFrame((nextTime) => this.render(nextTime));
|
|
184
360
|
}
|
|
361
|
+
|
|
362
|
+
updateSafeMode(frameMs) {
|
|
363
|
+
const nextState = nextSafeModeState({
|
|
364
|
+
state: this.safeModeState,
|
|
365
|
+
frameMs,
|
|
366
|
+
enabled: this.visualSettings.safeMode,
|
|
367
|
+
thresholdMs: this.visualSettings.safeModeFrameMs,
|
|
368
|
+
});
|
|
369
|
+
if (nextState.active === this.safeModeState.active) {
|
|
370
|
+
this.safeModeState = nextState;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
this.safeModeState = nextState;
|
|
375
|
+
this.resize();
|
|
376
|
+
dispatchRendererEvent(RENDERER_SAFE_MODE_EVENT, {
|
|
377
|
+
active: nextState.active,
|
|
378
|
+
effectiveDevicePixelRatio: this.effectiveDevicePixelRatio(),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
dispose() {
|
|
383
|
+
if (this.resizeHandler && typeof window !== "undefined") {
|
|
384
|
+
window.removeEventListener("resize", this.resizeHandler);
|
|
385
|
+
this.resizeHandler = null;
|
|
386
|
+
}
|
|
387
|
+
this.layerManager?.dispose?.();
|
|
388
|
+
this.shaderManager?.dispose?.();
|
|
389
|
+
}
|
|
185
390
|
}
|
|
186
391
|
|
|
392
|
+
const normalizeLiveControlState = (state) => {
|
|
393
|
+
if (state === null || state === undefined || typeof state === "boolean") {
|
|
394
|
+
return { enabled: !!state };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
enabled: state.value !== undefined ? !!state.value : !!state.enabled,
|
|
399
|
+
fade: finiteFloat(state.fade),
|
|
400
|
+
release: finiteFloat(state.release),
|
|
401
|
+
color: normalizeLiveControlColor(state.color),
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const finiteFloat = (value) => {
|
|
406
|
+
const number = Number(value);
|
|
407
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const normalizeLiveControlColor = (value) => {
|
|
411
|
+
if (value == null) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (Array.isArray(value)) {
|
|
416
|
+
const channels = Array.from(value)
|
|
417
|
+
.slice(0, 4)
|
|
418
|
+
.map((channel) => Number(channel));
|
|
419
|
+
if (channels.length < 3 || channels.length > 4 || channels.some((channel) => !Number.isFinite(channel))) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const rgbValues = channels.slice(0, 3);
|
|
424
|
+
const alpha = channels[3];
|
|
425
|
+
const rgb = rgbValues.every((channel) => channel >= 0 && channel <= 1)
|
|
426
|
+
? rgbValues.map((channel) => clamp(channel, 0, 1))
|
|
427
|
+
: rgbValues.map((channel) => clamp(channel / 255, 0, 1));
|
|
428
|
+
const normalizedAlpha = alpha == null ? null : (alpha > 1 ? clamp(alpha / 255, 0, 1) : clamp(alpha, 0, 1));
|
|
429
|
+
|
|
430
|
+
return channels.length === 3 ? rgb : [...rgb, normalizedAlpha];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const color = parseHexColor(String(value || ""));
|
|
434
|
+
if (!Array.isArray(color)) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (color.length === 4) {
|
|
439
|
+
return color.map((channel) => clamp(channel, 0, 1));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (color.length === 3) {
|
|
443
|
+
return color.map((channel) => clamp(channel, 0, 1));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return null;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const parseHexColor = (value) => {
|
|
450
|
+
const raw = String(value || "").trim();
|
|
451
|
+
const match = raw.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/);
|
|
452
|
+
if (!match) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const rawHex = match[1];
|
|
457
|
+
const hex = rawHex.length === 3 || rawHex.length === 4
|
|
458
|
+
? rawHex.split("").map((entry) => `${entry}${entry}`).join("")
|
|
459
|
+
: rawHex;
|
|
460
|
+
|
|
461
|
+
const channels = [];
|
|
462
|
+
for (let index = 0; index < hex.length; index += 2) {
|
|
463
|
+
const value = Number.parseInt(hex.slice(index, index + 2), 16);
|
|
464
|
+
channels.push(value / 255);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return channels;
|
|
468
|
+
};
|
|
469
|
+
|
|
187
470
|
const resolveRotationSpeed = (layers, amplitude) => {
|
|
188
471
|
const layerWithSpeed = Array.isArray(layers)
|
|
189
472
|
? layers.find((layer) => Number.isFinite(Number(layer?.params?.rotation_speed)))
|
|
@@ -277,4 +560,111 @@ const isSilentAudio = (audio) => {
|
|
|
277
560
|
&& Number(drums.hihat || 0) <= 0;
|
|
278
561
|
};
|
|
279
562
|
|
|
563
|
+
export const resolveEffectiveDevicePixelRatio = ({
|
|
564
|
+
devicePixelRatio,
|
|
565
|
+
maxDevicePixelRatio = 2,
|
|
566
|
+
safeModeActive = false,
|
|
567
|
+
safeModeScale = 0.75,
|
|
568
|
+
} = {}) => {
|
|
569
|
+
const rawDpr = Number(devicePixelRatio);
|
|
570
|
+
const maxDpr = clamp(Number(maxDevicePixelRatio || 2), 0.5, 4);
|
|
571
|
+
const base = clamp(Number.isFinite(rawDpr) ? rawDpr : 1, 0.5, maxDpr);
|
|
572
|
+
if (!safeModeActive) {
|
|
573
|
+
return roundDpr(base);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const scale = clamp(Number(safeModeScale || 0.75), 0.25, 1);
|
|
577
|
+
return roundDpr(clamp(base * scale, 0.5, maxDpr));
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
export const nextSafeModeState = ({
|
|
581
|
+
state,
|
|
582
|
+
frameMs,
|
|
583
|
+
enabled = true,
|
|
584
|
+
thresholdMs = 34,
|
|
585
|
+
} = {}) => {
|
|
586
|
+
const current = state || {};
|
|
587
|
+
if (!enabled) {
|
|
588
|
+
return { active: false, slowFrames: 0, fastFrames: 0 };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const value = Number(frameMs);
|
|
592
|
+
const threshold = clamp(Number(thresholdMs || 34), 16, 250);
|
|
593
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
594
|
+
return { ...current };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const slowFrames = value > threshold ? Number(current.slowFrames || 0) + 1 : 0;
|
|
598
|
+
const fastFrames = value < threshold * 0.75 ? Number(current.fastFrames || 0) + 1 : 0;
|
|
599
|
+
const active = current.active ? fastFrames < 120 : slowFrames >= 12;
|
|
600
|
+
return {
|
|
601
|
+
active,
|
|
602
|
+
slowFrames: active ? 0 : slowFrames,
|
|
603
|
+
fastFrames: active ? fastFrames : 0,
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
export const collectRendererCapabilities = (gl, {
|
|
608
|
+
devicePixelRatio = currentDevicePixelRatio(),
|
|
609
|
+
effectiveDevicePixelRatio = devicePixelRatio,
|
|
610
|
+
} = {}) => {
|
|
611
|
+
const maxViewportDims = safeGetParameter(gl, gl?.MAX_VIEWPORT_DIMS) || [];
|
|
612
|
+
return {
|
|
613
|
+
webgl2: true,
|
|
614
|
+
devicePixelRatio: roundDpr(devicePixelRatio),
|
|
615
|
+
effectiveDevicePixelRatio: roundDpr(effectiveDevicePixelRatio),
|
|
616
|
+
maxTextureSize: Number(safeGetParameter(gl, gl?.MAX_TEXTURE_SIZE) || 0),
|
|
617
|
+
maxDrawBuffers: Number(safeGetParameter(gl, gl?.MAX_DRAW_BUFFERS) || 0),
|
|
618
|
+
maxRenderbufferSize: Number(safeGetParameter(gl, gl?.MAX_RENDERBUFFER_SIZE) || 0),
|
|
619
|
+
maxViewportDims: Array.from(maxViewportDims).map((value) => Number(value || 0)),
|
|
620
|
+
floatColorBuffer: !!safeGetExtension(gl, "EXT_color_buffer_float"),
|
|
621
|
+
textureFloat: !!safeGetExtension(gl, "OES_texture_float"),
|
|
622
|
+
};
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const currentDevicePixelRatio = () => {
|
|
626
|
+
if (typeof window === "undefined") {
|
|
627
|
+
return 1;
|
|
628
|
+
}
|
|
629
|
+
return Number(window.devicePixelRatio || 1);
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const safeGetParameter = (gl, parameter) => {
|
|
633
|
+
if (!gl || parameter === undefined || typeof gl.getParameter !== "function") {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
return gl.getParameter(parameter);
|
|
639
|
+
} catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const safeGetExtension = (gl, name) => {
|
|
645
|
+
if (!gl || typeof gl.getExtension !== "function") {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
return gl.getExtension(name);
|
|
651
|
+
} catch {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const dispatchRendererEvent = (type, detail) => {
|
|
657
|
+
if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (typeof CustomEvent !== "function") {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
window.dispatchEvent(new CustomEvent(type, { detail }));
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const roundDpr = (value) => Math.round(Number(value || 1) * 100) / 100;
|
|
669
|
+
|
|
280
670
|
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|