vizcore 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/frontend/index.html +24 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +447 -57
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +391 -10
- data/frontend/src/renderer/layer-manager.js +472 -71
- data/frontend/src/runtime-control-preset.js +44 -0
- data/frontend/src/scene-patches.js +159 -0
- data/frontend/src/shader-error-overlay.js +1 -0
- data/frontend/src/visuals/image-renderer.js +19 -0
- data/frontend/src/visuals/particle-system.js +10 -0
- data/frontend/src/visuals/shape-renderer.js +13 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -0
- data/frontend/src/visuals/text-renderer.js +13 -0
- data/frontend/src/websocket-client.js +6 -0
- data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
- data/lib/vizcore/analysis/feature_recorder.rb +117 -7
- data/lib/vizcore/analysis/feature_replay.rb +48 -9
- data/lib/vizcore/analysis/pipeline.rb +258 -9
- data/lib/vizcore/analysis/tap_tempo.rb +17 -2
- data/lib/vizcore/audio/calibration.rb +156 -0
- data/lib/vizcore/audio/file_input.rb +28 -0
- data/lib/vizcore/audio/input_manager.rb +36 -1
- data/lib/vizcore/audio/midi_input.rb +5 -0
- data/lib/vizcore/audio/ring_buffer.rb +22 -0
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/dsl_reference.rb +64 -8
- data/lib/vizcore/cli/plugin_checker.rb +93 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
- data/lib/vizcore/cli/scene_inspector.rb +35 -1
- data/lib/vizcore/cli/scene_validator.rb +487 -39
- data/lib/vizcore/cli/shader_template.rb +7 -2
- data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
- data/lib/vizcore/cli.rb +268 -15
- data/lib/vizcore/config.rb +40 -3
- data/lib/vizcore/control_preset.rb +29 -0
- data/lib/vizcore/deep_copy.rb +21 -0
- data/lib/vizcore/dsl/color_helpers.rb +155 -0
- data/lib/vizcore/dsl/engine.rb +219 -23
- data/lib/vizcore/dsl/layer_builder.rb +278 -15
- data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
- data/lib/vizcore/dsl/layout_helpers.rb +290 -0
- data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
- data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
- data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
- data/lib/vizcore/dsl/reaction_builder.rb +1 -0
- data/lib/vizcore/dsl/scene_builder.rb +83 -13
- data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
- data/lib/vizcore/dsl/style_builder.rb +3 -0
- data/lib/vizcore/dsl/timeline_builder.rb +91 -8
- data/lib/vizcore/dsl/transition_controller.rb +157 -18
- data/lib/vizcore/dsl.rb +2 -0
- data/lib/vizcore/layer_catalog.rb +1 -0
- data/lib/vizcore/plugin_asset_policy.rb +55 -0
- data/lib/vizcore/project_manifest.rb +12 -2
- data/lib/vizcore/renderer/render_sequence.rb +104 -13
- data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +469 -23
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +676 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +39 -16
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +33 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +55 -4
- metadata +18 -3
|
@@ -3,6 +3,9 @@ import { ShaderManager } from "./shader-manager.js";
|
|
|
3
3
|
import { applyShaderParamOverrides } from "../shader-param-controls.js";
|
|
4
4
|
import { applyShapeEditorOverrides } from "../shape-editor-controls.js";
|
|
5
5
|
|
|
6
|
+
export const RENDERER_CAPABILITIES_EVENT = "vizcore:renderer-capabilities";
|
|
7
|
+
export const RENDERER_SAFE_MODE_EVENT = "vizcore:renderer-safe-mode";
|
|
8
|
+
|
|
6
9
|
export class Engine {
|
|
7
10
|
constructor(canvas) {
|
|
8
11
|
this.canvas = canvas;
|
|
@@ -14,17 +17,44 @@ export class Engine {
|
|
|
14
17
|
this.currentRotationSpeed = 0.5;
|
|
15
18
|
this.mediaElement = null;
|
|
16
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
|
+
};
|
|
17
27
|
this.visualSettings = {
|
|
18
28
|
visualGain: 1,
|
|
19
29
|
bassBoost: 1,
|
|
20
30
|
smoothing: 0,
|
|
21
31
|
beatHoldMs: 180,
|
|
22
32
|
wobbleAmount: 1,
|
|
33
|
+
maxDevicePixelRatio: 2,
|
|
34
|
+
safeMode: true,
|
|
35
|
+
safeModeFrameMs: 34,
|
|
36
|
+
safeModeScale: 0.75,
|
|
23
37
|
};
|
|
24
38
|
this.visualAudioState = null;
|
|
25
39
|
this.liveControls = {
|
|
26
|
-
blackout: false,
|
|
27
|
-
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
|
+
},
|
|
28
58
|
};
|
|
29
59
|
this.runtimeGlobals = {};
|
|
30
60
|
this.shaderParamOverrides = {};
|
|
@@ -34,6 +64,7 @@ export class Engine {
|
|
|
34
64
|
audio: {
|
|
35
65
|
amplitude: 0,
|
|
36
66
|
bands: { sub: 0, low: 0, mid: 0, high: 0 },
|
|
67
|
+
band_peaks: { sub: 0, low: 0, mid: 0, high: 0 },
|
|
37
68
|
fft: [],
|
|
38
69
|
onset: 0,
|
|
39
70
|
onsets: { sub: 0, low: 0, mid: 0, high: 0 },
|
|
@@ -41,6 +72,14 @@ export class Engine {
|
|
|
41
72
|
beat: false,
|
|
42
73
|
beat_pulse: 0,
|
|
43
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,
|
|
44
83
|
bpm: 0
|
|
45
84
|
},
|
|
46
85
|
scene: {
|
|
@@ -58,12 +97,18 @@ export class Engine {
|
|
|
58
97
|
|
|
59
98
|
this.shaderManager = new ShaderManager(this.gl);
|
|
60
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);
|
|
61
105
|
|
|
62
106
|
this.gl.enable(this.gl.DEPTH_TEST);
|
|
63
107
|
this.gl.enable(this.gl.BLEND);
|
|
64
108
|
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
65
109
|
this.resize();
|
|
66
|
-
|
|
110
|
+
this.resizeHandler = () => this.resize();
|
|
111
|
+
window.addEventListener("resize", this.resizeHandler);
|
|
67
112
|
}
|
|
68
113
|
|
|
69
114
|
setAudioFrame(frame) {
|
|
@@ -83,13 +128,108 @@ export class Engine {
|
|
|
83
128
|
...this.visualSettings,
|
|
84
129
|
...settings,
|
|
85
130
|
};
|
|
131
|
+
if (this.gl) {
|
|
132
|
+
this.resize();
|
|
133
|
+
}
|
|
86
134
|
}
|
|
87
135
|
|
|
88
136
|
setLiveControls(controls = {}) {
|
|
137
|
+
const now = performance.now();
|
|
89
138
|
this.liveControls = {
|
|
90
|
-
blackout:
|
|
91
|
-
freeze:
|
|
139
|
+
blackout: normalizeLiveControlState(controls?.blackout),
|
|
140
|
+
freeze: normalizeLiveControlState(controls?.freeze),
|
|
92
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;
|
|
93
233
|
}
|
|
94
234
|
|
|
95
235
|
setRuntimeGlobals(globals = {}) {
|
|
@@ -110,8 +250,9 @@ export class Engine {
|
|
|
110
250
|
}
|
|
111
251
|
|
|
112
252
|
resize() {
|
|
113
|
-
const
|
|
114
|
-
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));
|
|
115
256
|
if (this.canvas.width === width && this.canvas.height === height) {
|
|
116
257
|
return;
|
|
117
258
|
}
|
|
@@ -120,6 +261,15 @@ export class Engine {
|
|
|
120
261
|
this.gl.viewport(0, 0, width, height);
|
|
121
262
|
}
|
|
122
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
|
+
|
|
123
273
|
render(time) {
|
|
124
274
|
let deltaSeconds = (time - this.lastTime) / 1000;
|
|
125
275
|
this.lastTime = time;
|
|
@@ -142,14 +292,21 @@ export class Engine {
|
|
|
142
292
|
this.lastMediaTime = null;
|
|
143
293
|
}
|
|
144
294
|
|
|
145
|
-
|
|
146
|
-
|
|
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);
|
|
147
304
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
148
305
|
requestAnimationFrame((nextTime) => this.render(nextTime));
|
|
149
306
|
return;
|
|
150
307
|
}
|
|
151
308
|
|
|
152
|
-
if (this.
|
|
309
|
+
if (this.liveControlRuntime.freeze.enabled) {
|
|
153
310
|
requestAnimationFrame((nextTime) => this.render(nextTime));
|
|
154
311
|
return;
|
|
155
312
|
}
|
|
@@ -179,6 +336,16 @@ export class Engine {
|
|
|
179
336
|
);
|
|
180
337
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
181
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
|
+
|
|
182
349
|
this.layerManager.renderScene({
|
|
183
350
|
layers,
|
|
184
351
|
audio,
|
|
@@ -191,8 +358,115 @@ export class Engine {
|
|
|
191
358
|
|
|
192
359
|
requestAnimationFrame((nextTime) => this.render(nextTime));
|
|
193
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
|
+
}
|
|
194
390
|
}
|
|
195
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
|
+
|
|
196
470
|
const resolveRotationSpeed = (layers, amplitude) => {
|
|
197
471
|
const layerWithSpeed = Array.isArray(layers)
|
|
198
472
|
? layers.find((layer) => Number.isFinite(Number(layer?.params?.rotation_speed)))
|
|
@@ -286,4 +560,111 @@ const isSilentAudio = (audio) => {
|
|
|
286
560
|
&& Number(drums.hihat || 0) <= 0;
|
|
287
561
|
};
|
|
288
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
|
+
|
|
289
670
|
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|