vizcore 0.1.0 → 1.1.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +70 -117
  3. data/docs/.nojekyll +0 -0
  4. data/docs/assets/playground-worker.js +373 -0
  5. data/docs/assets/playground.css +440 -0
  6. data/docs/assets/playground.js +652 -0
  7. data/docs/assets/site.css +744 -0
  8. data/docs/assets/vizcore-demo.gif +0 -0
  9. data/docs/assets/vizcore-poster.png +0 -0
  10. data/docs/assets/vj-tunnel.js +159 -0
  11. data/docs/index.html +225 -0
  12. data/docs/playground.html +81 -0
  13. data/docs/shape_dsl.md +269 -0
  14. data/examples/README.md +59 -0
  15. data/examples/assets/README.md +19 -0
  16. data/examples/audio_inspector.rb +34 -0
  17. data/examples/club_intro_drop.rb +78 -0
  18. data/examples/kansai_rubykaigi_visual.rb +70 -0
  19. data/examples/live_coding_minimal.rb +22 -0
  20. data/examples/midi_controller_show.rb +78 -0
  21. data/examples/midi_scene_switch.rb +3 -1
  22. data/examples/parser_visualizer.rb +48 -0
  23. data/examples/readme_demo.rb +17 -0
  24. data/examples/rhythm_geometry.rb +34 -0
  25. data/examples/ruby_crystal_show.rb +35 -0
  26. data/examples/shader_playground.rb +18 -0
  27. data/examples/unyo_liquid.rb +59 -0
  28. data/examples/vj_ambient_chill_room.rb +124 -0
  29. data/examples/vj_dnb_jungle.rb +170 -0
  30. data/examples/vj_festival_mainstage.rb +245 -0
  31. data/examples/vj_festival_mainstage.yml +17 -0
  32. data/examples/vj_glitch_industrial.rb +164 -0
  33. data/examples/vj_hiphop_cipher.rb +167 -0
  34. data/examples/vj_jpop_idol_live.rb +210 -0
  35. data/examples/vj_synthwave_retro.rb +173 -0
  36. data/examples/vj_techno_warehouse.rb +195 -0
  37. data/frontend/index.html +494 -2
  38. data/frontend/src/audio-inspector.js +40 -0
  39. data/frontend/src/custom-shape-param-controls.js +106 -0
  40. data/frontend/src/live-controls.js +131 -0
  41. data/frontend/src/main.js +1060 -16
  42. data/frontend/src/mapping-target-selector.js +109 -0
  43. data/frontend/src/midi-learn.js +194 -0
  44. data/frontend/src/performance-monitor.js +183 -0
  45. data/frontend/src/plugin-runtime.js +130 -0
  46. data/frontend/src/projector-mode.js +56 -0
  47. data/frontend/src/renderer/engine.js +157 -3
  48. data/frontend/src/renderer/layer-manager.js +442 -30
  49. data/frontend/src/renderer/shader-manager.js +26 -0
  50. data/frontend/src/runtime-control-preset.js +11 -0
  51. data/frontend/src/shader-error-overlay.js +29 -0
  52. data/frontend/src/shader-param-controls.js +93 -0
  53. data/frontend/src/shaders/builtins.js +380 -2
  54. data/frontend/src/shaders/post-effects.js +52 -0
  55. data/frontend/src/shape-editor-controls.js +157 -0
  56. data/frontend/src/visual-regression.js +67 -0
  57. data/frontend/src/visual-settings-preset.js +103 -0
  58. data/frontend/src/visuals/geometry.js +666 -0
  59. data/frontend/src/visuals/image-renderer.js +291 -0
  60. data/frontend/src/visuals/particle-system.js +56 -10
  61. data/frontend/src/visuals/shape-renderer.js +475 -0
  62. data/frontend/src/visuals/spectrogram-renderer.js +226 -0
  63. data/frontend/src/visuals/svg-arc.js +104 -0
  64. data/frontend/src/visuals/text-renderer.js +112 -11
  65. data/frontend/src/websocket-client.js +12 -1
  66. data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
  67. data/lib/vizcore/analysis/beat_detector.rb +4 -2
  68. data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
  69. data/lib/vizcore/analysis/feature_recorder.rb +159 -0
  70. data/lib/vizcore/analysis/feature_replay.rb +84 -0
  71. data/lib/vizcore/analysis/pipeline.rb +235 -11
  72. data/lib/vizcore/analysis/tap_tempo.rb +74 -0
  73. data/lib/vizcore/analysis.rb +4 -0
  74. data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
  75. data/lib/vizcore/audio/fixture_input.rb +65 -0
  76. data/lib/vizcore/audio/input_manager.rb +4 -2
  77. data/lib/vizcore/audio/mic_input.rb +24 -8
  78. data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
  79. data/lib/vizcore/audio.rb +1 -0
  80. data/lib/vizcore/cli/doctor.rb +159 -0
  81. data/lib/vizcore/cli/dsl_reference.rb +99 -0
  82. data/lib/vizcore/cli/layer_docs.rb +46 -0
  83. data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
  84. data/lib/vizcore/cli/scene_inspector.rb +136 -0
  85. data/lib/vizcore/cli/scene_validator.rb +337 -0
  86. data/lib/vizcore/cli/shader_template.rb +68 -0
  87. data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
  88. data/lib/vizcore/cli.rb +689 -18
  89. data/lib/vizcore/config.rb +103 -2
  90. data/lib/vizcore/control_preset.rb +68 -0
  91. data/lib/vizcore/dsl/engine.rb +277 -5
  92. data/lib/vizcore/dsl/layer_builder.rb +1280 -23
  93. data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
  94. data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
  95. data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
  96. data/lib/vizcore/dsl/reaction_builder.rb +44 -0
  97. data/lib/vizcore/dsl/scene_builder.rb +61 -5
  98. data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
  99. data/lib/vizcore/dsl/style_builder.rb +68 -0
  100. data/lib/vizcore/dsl/timeline_builder.rb +138 -0
  101. data/lib/vizcore/dsl/transition_controller.rb +77 -0
  102. data/lib/vizcore/dsl.rb +5 -1
  103. data/lib/vizcore/layer_catalog.rb +275 -0
  104. data/lib/vizcore/project_manifest.rb +152 -0
  105. data/lib/vizcore/renderer/png_writer.rb +57 -0
  106. data/lib/vizcore/renderer/render_sequence.rb +153 -0
  107. data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
  108. data/lib/vizcore/renderer/scene_serializer.rb +36 -3
  109. data/lib/vizcore/renderer/snapshot.rb +38 -0
  110. data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
  111. data/lib/vizcore/renderer.rb +5 -0
  112. data/lib/vizcore/server/frame_broadcaster.rb +143 -8
  113. data/lib/vizcore/server/gallery_app.rb +155 -0
  114. data/lib/vizcore/server/gallery_page.rb +100 -0
  115. data/lib/vizcore/server/gallery_runner.rb +48 -0
  116. data/lib/vizcore/server/rack_app.rb +203 -4
  117. data/lib/vizcore/server/runner.rb +391 -22
  118. data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
  119. data/lib/vizcore/server/websocket_handler.rb +60 -10
  120. data/lib/vizcore/server.rb +4 -0
  121. data/lib/vizcore/shape.rb +719 -0
  122. data/lib/vizcore/sync/osc_message.rb +103 -0
  123. data/lib/vizcore/sync/osc_receiver.rb +68 -0
  124. data/lib/vizcore/sync.rb +4 -0
  125. data/lib/vizcore/templates/midi_control_scene.rb +3 -1
  126. data/lib/vizcore/templates/plugin_layer.rb +20 -0
  127. data/lib/vizcore/templates/plugin_readme.md +23 -0
  128. data/lib/vizcore/templates/plugin_renderer.js +43 -0
  129. data/lib/vizcore/templates/plugin_scene.rb +14 -0
  130. data/lib/vizcore/templates/project_readme.md +7 -23
  131. data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
  132. data/lib/vizcore/version.rb +1 -1
  133. data/lib/vizcore.rb +28 -0
  134. data/scripts/browser_capture.mjs +75 -0
  135. data/sig/vizcore.rbs +461 -0
  136. metadata +94 -3
  137. data/docs/GETTING_STARTED.md +0 -105
data/frontend/index.html CHANGED
@@ -51,6 +51,8 @@
51
51
  background: var(--panel-bg);
52
52
  backdrop-filter: blur(6px);
53
53
  min-width: 18rem;
54
+ max-height: calc(100vh - 2rem);
55
+ overflow-y: auto;
54
56
  }
55
57
 
56
58
  .hud h1 {
@@ -122,9 +124,334 @@
122
124
  opacity: 0.6;
123
125
  cursor: not-allowed;
124
126
  }
127
+
128
+ .live-controls {
129
+ margin-top: 0.5rem;
130
+ display: grid;
131
+ grid-template-columns: repeat(2, minmax(0, 1fr));
132
+ gap: 0.35rem;
133
+ }
134
+
135
+ .live-controls button {
136
+ margin-top: 0;
137
+ padding: 0.42rem 0.5rem;
138
+ font-weight: 700;
139
+ text-transform: uppercase;
140
+ }
141
+
142
+ .live-controls button.is-active {
143
+ border-color: rgba(248, 250, 252, 0.52);
144
+ color: #fff7ed;
145
+ }
146
+
147
+ #blackout-toggle.is-active {
148
+ background: rgba(127, 29, 29, 0.78);
149
+ }
150
+
151
+ #freeze-toggle.is-active {
152
+ background: rgba(133, 77, 14, 0.78);
153
+ }
154
+
155
+ .hud .live-controls__status {
156
+ grid-column: 1 / -1;
157
+ margin-top: 0;
158
+ color: #d9e8ff;
159
+ font-variant-numeric: tabular-nums;
160
+ }
161
+
162
+ .hud .performance-monitor {
163
+ margin-top: 0.45rem;
164
+ padding-top: 0.45rem;
165
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
166
+ color: #a8d8ff;
167
+ font-variant-numeric: tabular-nums;
168
+ line-height: 1.35;
169
+ }
170
+
171
+ .reactivity-controls {
172
+ margin-top: 0.55rem;
173
+ padding-top: 0.55rem;
174
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
175
+ }
176
+
177
+ .reactivity-actions {
178
+ display: flex;
179
+ flex-wrap: wrap;
180
+ gap: 0.45rem;
181
+ margin-top: 0.45rem;
182
+ }
183
+
184
+ .reactivity-actions button,
185
+ .midi-learn button {
186
+ border: 1px solid rgba(148, 163, 184, 0.28);
187
+ border-radius: 6px;
188
+ background: rgba(15, 23, 42, 0.68);
189
+ color: #d9e8ff;
190
+ cursor: pointer;
191
+ font: inherit;
192
+ padding: 0.28rem 0.62rem;
193
+ }
194
+
195
+ .reactivity-actions button:hover,
196
+ .midi-learn button:hover {
197
+ border-color: rgba(130, 255, 196, 0.55);
198
+ color: #d9ffe8;
199
+ }
200
+
201
+ .midi-learn {
202
+ margin-top: 0.55rem;
203
+ padding-top: 0.55rem;
204
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
205
+ }
206
+
207
+ .midi-learn__actions {
208
+ display: grid;
209
+ grid-template-columns: repeat(2, minmax(0, 1fr));
210
+ gap: 0.35rem;
211
+ margin-top: 0.4rem;
212
+ }
213
+
214
+ .midi-learn button {
215
+ margin-top: 0;
216
+ width: 100%;
217
+ font-size: 0.76rem;
218
+ }
219
+
220
+ .audio-inspector {
221
+ margin-top: 0.55rem;
222
+ padding-top: 0.55rem;
223
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
224
+ }
225
+
226
+ .audio-inspector__header,
227
+ .audio-meter {
228
+ display: grid;
229
+ grid-template-columns: 3.25rem minmax(7rem, 1fr) 3.25rem;
230
+ align-items: center;
231
+ gap: 0.45rem;
232
+ }
233
+
234
+ .audio-inspector__header {
235
+ margin-bottom: 0.35rem;
236
+ color: #d9e8ff;
237
+ font-size: 0.72rem;
238
+ font-weight: 700;
239
+ letter-spacing: 0.08em;
240
+ text-transform: uppercase;
241
+ }
242
+
243
+ .audio-inspector__header span:last-child {
244
+ grid-column: 2 / 4;
245
+ justify-self: end;
246
+ color: var(--muted);
247
+ font-weight: 500;
248
+ letter-spacing: 0;
249
+ text-transform: none;
250
+ }
251
+
252
+ .audio-meter {
253
+ margin-top: 0.24rem;
254
+ color: #aebfda;
255
+ font-size: 0.72rem;
256
+ font-variant-numeric: tabular-nums;
257
+ }
258
+
259
+ .meter-track {
260
+ position: relative;
261
+ height: 0.34rem;
262
+ overflow: hidden;
263
+ border-radius: 999px;
264
+ background: rgba(148, 163, 184, 0.16);
265
+ }
266
+
267
+ .meter-fill {
268
+ position: absolute;
269
+ inset: 0;
270
+ border-radius: inherit;
271
+ transform: scaleX(var(--meter-value, 0));
272
+ transform-origin: left center;
273
+ transition: transform 90ms linear;
274
+ background: linear-gradient(90deg, #65ffb0, #38bdf8 58%, #f472b6);
275
+ }
276
+
277
+ .fft-preview {
278
+ display: grid;
279
+ grid-template-columns: repeat(16, minmax(0, 1fr));
280
+ align-items: end;
281
+ gap: 0.12rem;
282
+ height: 2.45rem;
283
+ margin-top: 0.48rem;
284
+ padding: 0.18rem 0;
285
+ }
286
+
287
+ .fft-bar {
288
+ height: 100%;
289
+ min-width: 2px;
290
+ border-radius: 2px 2px 0 0;
291
+ transform: scaleY(var(--bin-value, 0));
292
+ transform-origin: bottom center;
293
+ transition: transform 90ms linear;
294
+ background: linear-gradient(180deg, #f8fafc, #38bdf8 45%, #65ffb0);
295
+ opacity: 0.9;
296
+ }
297
+
298
+ .shader-error {
299
+ position: absolute;
300
+ right: 1rem;
301
+ top: 1rem;
302
+ width: min(26rem, calc(100vw - 2rem));
303
+ max-height: calc(100vh - 2rem);
304
+ overflow: auto;
305
+ padding: 0.75rem;
306
+ border: 1px solid rgba(248, 113, 113, 0.45);
307
+ border-radius: 0.5rem;
308
+ background: rgba(20, 6, 10, 0.86);
309
+ box-shadow: 0 18px 48px rgba(0, 0, 0, 0.34);
310
+ color: #fee2e2;
311
+ backdrop-filter: blur(8px);
312
+ }
313
+
314
+ .shader-error[hidden] {
315
+ display: none;
316
+ }
317
+
318
+ .shader-error__header {
319
+ display: grid;
320
+ grid-template-columns: minmax(0, 1fr) auto;
321
+ align-items: center;
322
+ gap: 0.6rem;
323
+ margin-bottom: 0.45rem;
324
+ }
325
+
326
+ .shader-error h2 {
327
+ margin: 0;
328
+ overflow-wrap: anywhere;
329
+ color: #fecaca;
330
+ font-size: 0.88rem;
331
+ letter-spacing: 0;
332
+ }
333
+
334
+ .shader-error button {
335
+ min-width: 4rem;
336
+ border: 1px solid rgba(254, 202, 202, 0.4);
337
+ border-radius: 0.45rem;
338
+ padding: 0.32rem 0.52rem;
339
+ background: rgba(127, 29, 29, 0.5);
340
+ color: #fff1f2;
341
+ cursor: pointer;
342
+ }
343
+
344
+ .shader-error pre {
345
+ margin: 0;
346
+ white-space: pre-wrap;
347
+ overflow-wrap: anywhere;
348
+ color: #fed7aa;
349
+ font: 0.76rem/1.45 "SFMono-Regular", Consolas, monospace;
350
+ }
351
+
352
+ body.is-projector {
353
+ background: #000;
354
+ cursor: none;
355
+ }
356
+
357
+ body.is-projector .hud,
358
+ body.is-projector .shader-error {
359
+ display: none;
360
+ }
361
+
362
+ .reactivity-controls label,
363
+ .shader-param-controls label {
364
+ display: grid;
365
+ grid-template-columns: minmax(5.5rem, 0.8fr) minmax(8rem, 1.2fr) minmax(2.8rem, 0.35fr);
366
+ align-items: center;
367
+ gap: 0.5rem;
368
+ margin-top: 0.3rem;
369
+ font-size: 0.78rem;
370
+ color: #c9d9ee;
371
+ }
372
+
373
+ .reactivity-controls label {
374
+ grid-template-columns: minmax(5.5rem, 0.8fr) minmax(8rem, 1.2fr);
375
+ }
376
+
377
+ .reactivity-controls input[type="range"],
378
+ .shader-param-controls input[type="range"] {
379
+ width: 100%;
380
+ }
381
+
382
+ .reactivity-controls p,
383
+ .shader-param-controls p {
384
+ margin-top: 0.45rem;
385
+ line-height: 1.35;
386
+ }
387
+
388
+ .shader-param-controls {
389
+ margin-top: 0.55rem;
390
+ padding-top: 0.55rem;
391
+ border-top: 1px solid rgba(153, 195, 255, 0.16);
392
+ }
393
+
394
+ .shader-param-controls[hidden] {
395
+ display: none;
396
+ }
397
+
398
+ .shader-param-controls__title {
399
+ color: #d9e8ff;
400
+ font-size: 0.72rem;
401
+ font-weight: 700;
402
+ letter-spacing: 0.08em;
403
+ text-transform: uppercase;
404
+ }
405
+
406
+ .shader-param-controls output {
407
+ color: #a8d8ff;
408
+ font-variant-numeric: tabular-nums;
409
+ text-align: right;
410
+ }
411
+
412
+ .shader-param-controls details {
413
+ margin-top: 0.35rem;
414
+ padding-top: 0.3rem;
415
+ border-top: 1px solid rgba(153, 195, 255, 0.12);
416
+ }
417
+
418
+ .shader-param-controls summary {
419
+ cursor: pointer;
420
+ color: #d9e8ff;
421
+ font-size: 0.78rem;
422
+ font-weight: 700;
423
+ }
424
+
425
+ .shader-param-controls select,
426
+ .shader-param-controls input[type="color"] {
427
+ width: 100%;
428
+ min-height: 1.65rem;
429
+ border: 1px solid rgba(153, 195, 255, 0.28);
430
+ border-radius: 0.35rem;
431
+ background: rgba(2, 6, 14, 0.72);
432
+ color: var(--fg);
433
+ }
434
+
435
+ @media (max-width: 520px) {
436
+ .hud {
437
+ right: 1rem;
438
+ min-width: 0;
439
+ }
440
+
441
+ .audio-inspector__header,
442
+ .audio-meter {
443
+ grid-template-columns: 2.75rem minmax(5rem, 1fr) 2.9rem;
444
+ }
445
+
446
+ .shader-error {
447
+ left: 1rem;
448
+ top: auto;
449
+ bottom: 1rem;
450
+ }
451
+ }
125
452
  </style>
126
453
  </head>
127
- <body>
454
+ <body data-projector-mode="false" data-display-mode="auto">
128
455
  <div id="app">
129
456
  <canvas id="vizcore-canvas"></canvas>
130
457
  <section class="hud" aria-live="polite">
@@ -135,14 +462,179 @@
135
462
  <p id="frame-status">Amplitude: 0.0000</p>
136
463
  <p id="bpm-status" class="is-accent">BPM: --</p>
137
464
  <p id="beat-status">Beat: off | Count: 0</p>
465
+ <div class="live-controls" aria-label="Live emergency controls">
466
+ <button id="blackout-toggle" type="button" aria-pressed="false">Blackout</button>
467
+ <button id="freeze-toggle" type="button" aria-pressed="false">Freeze</button>
468
+ <p id="live-control-status" class="live-controls__status">Live: output</p>
469
+ </div>
470
+ <p id="performance-monitor" class="performance-monitor">Perf: -- FPS | Frame -- | WS -- | RTT -- | Clock -- | Drop 0 | Audio -- | Shader -- | Reconnect 0</p>
471
+ <div class="audio-inspector" aria-label="Audio inspector">
472
+ <div class="audio-inspector__header">
473
+ <span>Audio</span>
474
+ <span id="inspector-peak">Peak: --</span>
475
+ </div>
476
+ <div class="audio-meter">
477
+ <span>amp</span>
478
+ <span class="meter-track"><span id="inspector-amplitude-fill" class="meter-fill"></span></span>
479
+ <span id="inspector-amplitude-value">0.000</span>
480
+ </div>
481
+ <div class="audio-meter">
482
+ <span>sub</span>
483
+ <span class="meter-track"><span id="inspector-band-sub-fill" class="meter-fill"></span></span>
484
+ <span id="inspector-band-sub-value">0.00</span>
485
+ </div>
486
+ <div class="audio-meter">
487
+ <span>low</span>
488
+ <span class="meter-track"><span id="inspector-band-low-fill" class="meter-fill"></span></span>
489
+ <span id="inspector-band-low-value">0.00</span>
490
+ </div>
491
+ <div class="audio-meter">
492
+ <span>mid</span>
493
+ <span class="meter-track"><span id="inspector-band-mid-fill" class="meter-fill"></span></span>
494
+ <span id="inspector-band-mid-value">0.00</span>
495
+ </div>
496
+ <div class="audio-meter">
497
+ <span>high</span>
498
+ <span class="meter-track"><span id="inspector-band-high-fill" class="meter-fill"></span></span>
499
+ <span id="inspector-band-high-value">0.00</span>
500
+ </div>
501
+ <div id="fft-preview" class="fft-preview" aria-label="FFT preview"></div>
502
+ </div>
138
503
  <p id="audio-source-status">Audio Source: unknown</p>
139
504
  <p id="audio-track-status">Track: none</p>
140
505
  <p id="audio-playback-status">Playback: unavailable</p>
506
+ <div class="reactivity-controls" aria-label="Visual reactivity controls">
507
+ <label>Visual Gain <input id="visual-gain-control" type="range" min="1" max="8" step="0.1" value="2.5" /></label>
508
+ <label>Bass Boost <input id="bass-boost-control" type="range" min="0" max="4" step="0.1" value="1.4" /></label>
509
+ <label>Smoothing <input id="smoothing-control" type="range" min="0" max="0.9" step="0.05" value="0.25" /></label>
510
+ <label>Beat Hold <input id="beat-hold-control" type="range" min="50" max="400" step="10" value="180" /></label>
511
+ <label>Wobble <input id="wobble-control" type="range" min="0.25" max="3" step="0.05" value="1" /></label>
512
+ <div class="reactivity-actions" aria-label="Reactivity preset controls">
513
+ <button id="reactivity-save" type="button">Save</button>
514
+ <button id="reactivity-load" type="button">Load</button>
515
+ <button id="reactivity-project-save" type="button" hidden>Save Project</button>
516
+ <button id="reactivity-export" type="button">Export</button>
517
+ <button id="reactivity-import" type="button">Import</button>
518
+ </div>
519
+ <p id="reactivity-status">Visual Gain: 2.5x | Bass: 1.4x | Smooth: 0.25 | Beat Hold: 180ms | Wobble: 1.00x</p>
520
+ </div>
521
+ <div class="midi-learn" aria-label="MIDI learn controls">
522
+ <p id="midi-learn-status">MIDI Learn: idle | Bindings: 0</p>
523
+ <div class="midi-learn__actions">
524
+ <button type="button" data-midi-learn-action="current-scene">Learn Scene</button>
525
+ <button type="button" data-midi-learn-action="blackout">Learn Blackout</button>
526
+ <button type="button" data-midi-learn-action="freeze">Learn Freeze</button>
527
+ <button type="button" data-midi-learn-action="visual:visualGain">Learn Gain</button>
528
+ <button type="button" data-midi-learn-action="visual:bassBoost">Learn Bass</button>
529
+ <button type="button" data-midi-learn-action="visual:wobbleAmount">Learn Wobble</button>
530
+ </div>
531
+ </div>
532
+ <div id="shader-param-controls" class="shader-param-controls" hidden aria-label="Shader parameter controls"></div>
533
+ <div id="shape-editor-controls" class="shader-param-controls" hidden aria-label="Shape editor controls"></div>
534
+ <div id="custom-shape-param-controls" class="shader-param-controls" hidden aria-label="Custom shape parameter controls"></div>
535
+ <div id="mapping-target-selector" class="shader-param-controls" hidden aria-label="Mapping target selector"></div>
141
536
  <div id="scene-switcher" class="scene-switcher" hidden aria-label="Scene switcher"></div>
142
537
  <button id="audio-toggle" type="button" hidden>Play Audio</button>
143
538
  </section>
539
+ <section id="shader-error-overlay" class="shader-error" aria-live="assertive" hidden>
540
+ <div class="shader-error__header">
541
+ <h2 id="shader-error-title">Shader Error</h2>
542
+ <button id="shader-error-close" type="button">Close</button>
543
+ </div>
544
+ <pre id="shader-error-message"></pre>
545
+ </section>
144
546
  </div>
145
547
 
146
- <script type="module" src="/src/main.js"></script>
548
+ <script>
549
+ window.__vizcoreMainStarted = false;
550
+ (function () {
551
+ var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
552
+ var websocketUrl = protocol + "//" + window.location.host + "/ws";
553
+ var fallbackTimer = window.setTimeout(function () {
554
+ if (window.__vizcoreMainStarted) return;
555
+
556
+ var wsStatus = document.getElementById("ws-status");
557
+ var sceneStatus = document.getElementById("scene-status");
558
+ var frameStatus = document.getElementById("frame-status");
559
+ var bpmStatus = document.getElementById("bpm-status");
560
+ var beatStatus = document.getElementById("beat-status");
561
+ var audioSourceStatus = document.getElementById("audio-source-status");
562
+ var peakStatus = document.getElementById("inspector-peak");
563
+ var ampValue = document.getElementById("inspector-amplitude-value");
564
+ var ampFill = document.getElementById("inspector-amplitude-fill");
565
+ var bands = ["sub", "low", "mid", "high"];
566
+
567
+ function setText(element, text) {
568
+ if (element) element.textContent = text;
569
+ }
570
+
571
+ function setMeter(id, value) {
572
+ var fill = document.getElementById("inspector-band-" + id + "-fill");
573
+ var label = document.getElementById("inspector-band-" + id + "-value");
574
+ if (fill) fill.style.setProperty("--meter-value", value.toFixed(4));
575
+ if (label) label.textContent = value.toFixed(2);
576
+ }
577
+
578
+ setText(wsStatus, "WebSocket: fallback connecting (" + websocketUrl + ")");
579
+ window.fetch("/runtime", { cache: "no-store" })
580
+ .then(function (response) { return response.ok ? response.json() : null; })
581
+ .then(function (runtime) {
582
+ if (runtime && audioSourceStatus) {
583
+ audioSourceStatus.textContent = "Audio Source: " + runtime.audio_source;
584
+ }
585
+ })
586
+ .catch(function () {});
587
+
588
+ var socket = new WebSocket(websocketUrl);
589
+ socket.addEventListener("open", function () {
590
+ setText(wsStatus, "WebSocket: fallback connected (" + websocketUrl + ")");
591
+ });
592
+ socket.addEventListener("error", function () {
593
+ setText(wsStatus, "WebSocket: fallback error (" + websocketUrl + ")");
594
+ });
595
+ socket.addEventListener("close", function () {
596
+ setText(wsStatus, "WebSocket: fallback closed (" + websocketUrl + ")");
597
+ });
598
+ socket.addEventListener("message", function (event) {
599
+ var message;
600
+ try {
601
+ message = JSON.parse(event.data);
602
+ } catch (_error) {
603
+ return;
604
+ }
605
+ if (!message || message.type !== "audio_frame" || !message.payload) return;
606
+
607
+ var payload = message.payload;
608
+ var audio = payload.audio || {};
609
+ var scene = payload.scene || {};
610
+ var amplitude = Math.max(0, Math.min(1, Number(audio.amplitude || 0)));
611
+ var bpm = Number(audio.bpm || 0);
612
+ var beat = !!audio.beat;
613
+ var beatCount = Math.max(0, Number(audio.beat_count || 0) || 0);
614
+
615
+ setText(sceneStatus, "Scene: " + (scene.name || "unknown"));
616
+ setText(frameStatus, "Amplitude: " + amplitude.toFixed(4));
617
+ setText(bpmStatus, "BPM: " + (bpm > 0 ? bpm.toFixed(1) : "--"));
618
+ setText(beatStatus, "Beat: " + (beat ? "ON" : "off") + " | Count: " + beatCount);
619
+ if (ampValue) ampValue.textContent = amplitude.toFixed(3);
620
+ if (ampFill) ampFill.style.setProperty("--meter-value", amplitude.toFixed(4));
621
+ if (peakStatus) {
622
+ var peak = Number(audio.peak_frequency || 0);
623
+ peakStatus.textContent = peak > 0 ? "Peak: " + Math.round(peak) + " Hz" : "Peak: --";
624
+ }
625
+
626
+ var audioBands = audio.bands || {};
627
+ bands.forEach(function (key) {
628
+ setMeter(key, Math.max(0, Math.min(1, Number(audioBands[key] || 0))));
629
+ });
630
+ });
631
+ }, 1200);
632
+
633
+ window.addEventListener("beforeunload", function () {
634
+ window.clearTimeout(fallbackTimer);
635
+ });
636
+ })();
637
+ </script>
638
+ <script type="module" src="/src/main.js?v=20260516d"></script>
147
639
  </body>
148
640
  </html>
@@ -0,0 +1,40 @@
1
+ export const BAND_KEYS = ["sub", "low", "mid", "high"];
2
+ export const DEFAULT_FFT_BINS = 16;
3
+
4
+ export const buildAudioInspectorState = (audio, fftBins = DEFAULT_FFT_BINS) => {
5
+ const bands = BAND_KEYS.reduce((result, key) => {
6
+ result[key] = clamp01(audio?.bands?.[key]);
7
+ return result;
8
+ }, {});
9
+
10
+ return {
11
+ amplitude: clamp01(audio?.amplitude),
12
+ bands,
13
+ fft: normalizeFft(audio?.fft, fftBins),
14
+ bpm: Number(audio?.bpm || 0),
15
+ beat: !!audio?.beat,
16
+ beatPulse: clamp01(audio?.beat_pulse),
17
+ peakFrequency: Math.max(0, Number(audio?.peak_frequency || 0) || 0),
18
+ };
19
+ };
20
+
21
+ export const formatMeterValue = (value, digits = 2) => {
22
+ const numeric = Number(value);
23
+ if (!Number.isFinite(numeric)) {
24
+ return Number(0).toFixed(digits);
25
+ }
26
+ return numeric.toFixed(digits);
27
+ };
28
+
29
+ const normalizeFft = (value, size) => {
30
+ const input = Array.isArray(value) || ArrayBuffer.isView(value) ? Array.from(value) : [];
31
+ return Array.from({ length: size }, (_entry, index) => clamp01(input[index]));
32
+ };
33
+
34
+ const clamp01 = (value) => {
35
+ const numeric = Number(value);
36
+ if (!Number.isFinite(numeric)) {
37
+ return 0;
38
+ }
39
+ return Math.min(Math.max(numeric, 0), 1);
40
+ };
@@ -0,0 +1,106 @@
1
+ export const customShapeParamControlEntries = (layers, overrides = {}) => {
2
+ const entries = [];
3
+ const layerList = Array.isArray(layers) ? layers : [];
4
+
5
+ layerList.forEach((layer, layerIndex) => {
6
+ const controls = Array.isArray(layer?.params?.custom_shape_controls) ? layer.params.custom_shape_controls : [];
7
+ if (!controls.length) return;
8
+
9
+ const layerKey = layerControlKey(layer, layerIndex);
10
+ const layerName = String(layer?.name || `layer_${layerIndex + 1}`);
11
+ controls.forEach((control, controlIndex) => {
12
+ const customShapeIndex = finiteIndex(control?.index, controlIndex);
13
+ const customShapeName = String(control?.name || `custom_shape_${customShapeIndex + 1}`);
14
+ const params = control?.params && typeof control.params === "object" ? control.params : {};
15
+ const schemaByName = paramSchemaByName(control?.param_schema);
16
+ const paramNames = [...new Set([...Object.keys(schemaByName), ...Object.keys(params)])].sort();
17
+
18
+ paramNames.forEach((paramName) => {
19
+ const schema = schemaByName[paramName] || {};
20
+ const baseValue = finiteNumber(params[paramName], finiteNumber(schema.default, 0));
21
+ const min = finiteNumber(schema.min, Math.min(0, baseValue));
22
+ const fallbackMax = Math.max(min + 1, Math.abs(baseValue) * 2, 1);
23
+ const max = Math.max(min, finiteNumber(schema.max, fallbackMax));
24
+ const step = positiveNumber(schema.step, Math.max((max - min) / 100, 0.01));
25
+ const value = finiteNumber(overrides?.[layerKey]?.[customShapeIndex]?.[paramName], baseValue);
26
+
27
+ entries.push({
28
+ key: `${layerKey}:custom_shapes.${customShapeIndex}.params.${paramName}`,
29
+ layerKey,
30
+ layerName,
31
+ customShapeIndex,
32
+ customShapeName,
33
+ paramName,
34
+ label: `${layerName}.${customShapeName}.${paramName}`,
35
+ target: `custom_shapes.${customShapeIndex}.params.${paramName}`,
36
+ min,
37
+ max,
38
+ step,
39
+ value: clamp(value, min, max),
40
+ });
41
+ });
42
+ });
43
+ });
44
+
45
+ return entries;
46
+ };
47
+
48
+ export const pruneCustomShapeParamOverrides = (overrides = {}, entries = []) => {
49
+ const validKeys = new Set(entries.map((entry) => `${entry.layerKey}:${entry.customShapeIndex}:${entry.paramName}`));
50
+ const next = {};
51
+
52
+ Object.entries(overrides || {}).forEach(([layerKey, controls]) => {
53
+ if (!controls || typeof controls !== "object") return;
54
+
55
+ Object.entries(controls).forEach(([customShapeIndex, params]) => {
56
+ if (!params || typeof params !== "object") return;
57
+
58
+ Object.entries(params).forEach(([paramName, value]) => {
59
+ if (!validKeys.has(`${layerKey}:${customShapeIndex}:${paramName}`) || !Number.isFinite(Number(value))) return;
60
+
61
+ next[layerKey] ||= {};
62
+ next[layerKey][customShapeIndex] ||= {};
63
+ next[layerKey][customShapeIndex][paramName] = Number(value);
64
+ });
65
+ });
66
+ });
67
+
68
+ return next;
69
+ };
70
+
71
+ export const customShapeParamMessage = (entry, value) => ({
72
+ layer: entry.layerName,
73
+ custom_shape_index: entry.customShapeIndex,
74
+ param: entry.paramName,
75
+ value: clamp(finiteNumber(value, entry.value), entry.min, entry.max),
76
+ });
77
+
78
+ const layerControlKey = (layer, index) => `${index}:${String(layer?.name || "layer")}`;
79
+
80
+ const paramSchemaByName = (schema) => {
81
+ const output = {};
82
+ (Array.isArray(schema) ? schema : []).forEach((entry) => {
83
+ const name = String(entry?.name || "").trim();
84
+ if (!name) return;
85
+
86
+ output[name] = entry;
87
+ });
88
+ return output;
89
+ };
90
+
91
+ const finiteIndex = (value, fallback) => {
92
+ const numeric = Number(value);
93
+ return Number.isInteger(numeric) && numeric >= 0 ? numeric : fallback;
94
+ };
95
+
96
+ const finiteNumber = (value, fallback) => {
97
+ const numeric = Number(value);
98
+ return Number.isFinite(numeric) ? numeric : fallback;
99
+ };
100
+
101
+ const positiveNumber = (value, fallback) => {
102
+ const numeric = finiteNumber(value, fallback);
103
+ return numeric > 0 ? numeric : fallback;
104
+ };
105
+
106
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);