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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/index.html +24 -2
  3. data/frontend/src/audio-inspector.js +9 -0
  4. data/frontend/src/live-controls.js +219 -7
  5. data/frontend/src/main.js +447 -57
  6. data/frontend/src/midi-learn.js +22 -2
  7. data/frontend/src/performance-monitor.js +137 -1
  8. data/frontend/src/renderer/engine.js +391 -10
  9. data/frontend/src/renderer/layer-manager.js +472 -71
  10. data/frontend/src/runtime-control-preset.js +44 -0
  11. data/frontend/src/scene-patches.js +159 -0
  12. data/frontend/src/shader-error-overlay.js +1 -0
  13. data/frontend/src/visuals/image-renderer.js +19 -0
  14. data/frontend/src/visuals/particle-system.js +10 -0
  15. data/frontend/src/visuals/shape-renderer.js +13 -0
  16. data/frontend/src/visuals/spectrogram-renderer.js +14 -0
  17. data/frontend/src/visuals/text-renderer.js +13 -0
  18. data/frontend/src/websocket-client.js +6 -0
  19. data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
  20. data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
  21. data/lib/vizcore/analysis/feature_recorder.rb +117 -7
  22. data/lib/vizcore/analysis/feature_replay.rb +48 -9
  23. data/lib/vizcore/analysis/pipeline.rb +258 -9
  24. data/lib/vizcore/analysis/tap_tempo.rb +17 -2
  25. data/lib/vizcore/audio/calibration.rb +156 -0
  26. data/lib/vizcore/audio/file_input.rb +28 -0
  27. data/lib/vizcore/audio/input_manager.rb +36 -1
  28. data/lib/vizcore/audio/midi_input.rb +5 -0
  29. data/lib/vizcore/audio/ring_buffer.rb +22 -0
  30. data/lib/vizcore/audio.rb +1 -0
  31. data/lib/vizcore/cli/dsl_reference.rb +64 -8
  32. data/lib/vizcore/cli/plugin_checker.rb +93 -0
  33. data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
  34. data/lib/vizcore/cli/scene_inspector.rb +35 -1
  35. data/lib/vizcore/cli/scene_validator.rb +487 -39
  36. data/lib/vizcore/cli/shader_template.rb +7 -2
  37. data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
  38. data/lib/vizcore/cli.rb +268 -15
  39. data/lib/vizcore/config.rb +40 -3
  40. data/lib/vizcore/control_preset.rb +29 -0
  41. data/lib/vizcore/deep_copy.rb +21 -0
  42. data/lib/vizcore/dsl/color_helpers.rb +155 -0
  43. data/lib/vizcore/dsl/engine.rb +219 -23
  44. data/lib/vizcore/dsl/layer_builder.rb +278 -15
  45. data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
  46. data/lib/vizcore/dsl/layout_helpers.rb +290 -0
  47. data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
  48. data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
  49. data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
  50. data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
  51. data/lib/vizcore/dsl/reaction_builder.rb +1 -0
  52. data/lib/vizcore/dsl/scene_builder.rb +83 -13
  53. data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
  54. data/lib/vizcore/dsl/style_builder.rb +3 -0
  55. data/lib/vizcore/dsl/timeline_builder.rb +91 -8
  56. data/lib/vizcore/dsl/transition_controller.rb +157 -18
  57. data/lib/vizcore/dsl.rb +2 -0
  58. data/lib/vizcore/layer_catalog.rb +1 -0
  59. data/lib/vizcore/plugin_asset_policy.rb +55 -0
  60. data/lib/vizcore/project_manifest.rb +12 -2
  61. data/lib/vizcore/renderer/render_sequence.rb +104 -13
  62. data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
  63. data/lib/vizcore/renderer/scene_serializer.rb +38 -0
  64. data/lib/vizcore/renderer/snapshot.rb +4 -3
  65. data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
  66. data/lib/vizcore/scene_trust.rb +31 -0
  67. data/lib/vizcore/server/frame_broadcaster.rb +469 -23
  68. data/lib/vizcore/server/rack_app.rb +151 -4
  69. data/lib/vizcore/server/runner.rb +676 -82
  70. data/lib/vizcore/server/websocket_handler.rb +236 -14
  71. data/lib/vizcore/server.rb +21 -0
  72. data/lib/vizcore/shape.rb +39 -16
  73. data/lib/vizcore/sync/osc_message.rb +66 -9
  74. data/lib/vizcore/version.rb +1 -1
  75. data/lib/vizcore.rb +33 -0
  76. data/scripts/browser_capture.mjs +31 -2
  77. data/sig/vizcore.rbs +55 -4
  78. 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
- window.addEventListener("resize", () => this.resize());
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: !!controls?.blackout,
91
- freeze: !!controls?.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 width = Math.floor(this.canvas.clientWidth * window.devicePixelRatio);
114
- const height = Math.floor(this.canvas.clientHeight * window.devicePixelRatio);
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
- if (this.liveControls.blackout) {
146
- this.gl.clearColor(0, 0, 0, 1);
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.liveControls.freeze) {
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);