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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="description" content="Vizcore Ruby wasm playground for editing Ruby scene DSL and previewing audio-reactive visuals in the browser." />
|
|
7
|
+
<meta name="theme-color" content="#08110f" />
|
|
8
|
+
<link rel="canonical" href="https://ydah.github.io/vizcore/playground.html" />
|
|
9
|
+
<meta property="og:type" content="website" />
|
|
10
|
+
<meta property="og:title" content="Vizcore Playground" />
|
|
11
|
+
<meta property="og:description" content="Edit Vizcore Ruby DSL and preview audio-reactive scenes in the browser." />
|
|
12
|
+
<meta property="og:url" content="https://ydah.github.io/vizcore/playground.html" />
|
|
13
|
+
<meta property="og:image" content="https://ydah.github.io/vizcore/assets/vizcore-poster.png" />
|
|
14
|
+
<title>Vizcore Playground</title>
|
|
15
|
+
<link rel="stylesheet" href="assets/playground.css" />
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<a class="skip-link" href="#editor">Skip to editor</a>
|
|
19
|
+
|
|
20
|
+
<header class="topbar" aria-label="Playground header">
|
|
21
|
+
<a class="brand" href="index.html">Vizcore</a>
|
|
22
|
+
<nav class="topbar-nav" aria-label="Playground navigation">
|
|
23
|
+
<a href="index.html">Home</a>
|
|
24
|
+
<a href="https://github.com/ydah/vizcore">GitHub</a>
|
|
25
|
+
</nav>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<main class="playground-shell">
|
|
29
|
+
<section class="editor-panel" aria-labelledby="editor-title">
|
|
30
|
+
<div class="panel-header">
|
|
31
|
+
<div>
|
|
32
|
+
<p class="eyebrow">Ruby wasm</p>
|
|
33
|
+
<h1 id="editor-title">Playground</h1>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="toolbar">
|
|
36
|
+
<label class="preset-label" for="preset-select">Preset</label>
|
|
37
|
+
<select id="preset-select" class="preset-select"></select>
|
|
38
|
+
<button id="run-button" class="button primary" type="button">Run</button>
|
|
39
|
+
<button id="reset-button" class="button secondary" type="button">Reset</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<textarea id="editor" class="code-editor" spellcheck="false" autocomplete="off" autocapitalize="off"></textarea>
|
|
44
|
+
|
|
45
|
+
<div class="output-row">
|
|
46
|
+
<div class="status-line" aria-live="polite">
|
|
47
|
+
<span id="ruby-status">Ruby wasm idle</span>
|
|
48
|
+
<span id="compile-status">Waiting</span>
|
|
49
|
+
</div>
|
|
50
|
+
<details class="json-output">
|
|
51
|
+
<summary>Scene JSON</summary>
|
|
52
|
+
<pre id="json-output">{}</pre>
|
|
53
|
+
</details>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
|
|
57
|
+
<section class="preview-panel" aria-labelledby="preview-title">
|
|
58
|
+
<div class="preview-header">
|
|
59
|
+
<div>
|
|
60
|
+
<p class="eyebrow">Canvas preview</p>
|
|
61
|
+
<h2 id="preview-title">Output</h2>
|
|
62
|
+
</div>
|
|
63
|
+
<div id="scene-tabs" class="scene-tabs" aria-label="Scenes"></div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="canvas-wrap">
|
|
67
|
+
<canvas id="preview-canvas" width="1280" height="720" aria-label="Vizcore visual preview"></canvas>
|
|
68
|
+
<div class="preview-meter" aria-live="polite">
|
|
69
|
+
<span id="scene-stat">Scene: --</span>
|
|
70
|
+
<span id="audio-stat">Amplitude: --</span>
|
|
71
|
+
<span id="beat-stat">Beat: --</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div id="error-output" class="error-output" role="alert" hidden></div>
|
|
76
|
+
</section>
|
|
77
|
+
</main>
|
|
78
|
+
|
|
79
|
+
<script type="module" src="assets/playground.js"></script>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
data/docs/shape_dsl.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Vizcore Shape DSL
|
|
2
|
+
|
|
3
|
+
Vizcore shape layers can declare 2D vector primitives directly in Ruby. Calling a
|
|
4
|
+
shape primitive inside a layer automatically marks that layer as `type: :shape`.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
Vizcore.define do
|
|
8
|
+
scene :logo do
|
|
9
|
+
layer :badge do
|
|
10
|
+
rect :panel, width: 360, height: 160, radius: 24 do
|
|
11
|
+
fill "#111827"
|
|
12
|
+
stroke width: 2, color: "#38bdf8"
|
|
13
|
+
opacity 0.8
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
star :spark, points: 5, radius: 72, inner_radius: 28 do
|
|
17
|
+
translate x: 180, y: 0
|
|
18
|
+
map beat_pulse, to: :scale, range: 0.8..1.4
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Primitives
|
|
26
|
+
|
|
27
|
+
Supported primitives:
|
|
28
|
+
|
|
29
|
+
- `circle id = nil, x: 0, y: 0, radius: 100, count: 1, segments: 96`
|
|
30
|
+
- `line id = nil, x1: -100, y1: 0, x2: 100, y2: 0`
|
|
31
|
+
- `rect id = nil, x: 0, y: 0, width:, height:, radius: 0`
|
|
32
|
+
- `polygon id = nil, points: [[x, y], ...], closed: true`
|
|
33
|
+
- `polyline id = nil, points: [[x, y], ...]`
|
|
34
|
+
- `path id = nil, detail: 32, tolerance: nil, max_segments: 4096 do ... end`
|
|
35
|
+
- `bezier id = nil, from:, control:, to:` for quadratic curves
|
|
36
|
+
- `bezier id = nil, from:, c1:, c2:, to:` for cubic curves
|
|
37
|
+
- `star id = nil, points: 5, radius: 100, inner_radius: 50`
|
|
38
|
+
- `custom_shape name_or_class, **params`
|
|
39
|
+
|
|
40
|
+
`draw do ... end` may be used to group declarations for readability.
|
|
41
|
+
`group do ... end` applies shared style and transform to child primitives.
|
|
42
|
+
|
|
43
|
+
## Path Commands
|
|
44
|
+
|
|
45
|
+
Path blocks support an SVG-like command subset:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
path :blob, detail: 48 do
|
|
49
|
+
move_to 0, 120
|
|
50
|
+
line_to 120, 0
|
|
51
|
+
quad_to 80, -100, 0, -120
|
|
52
|
+
cubic_to -80, -100, -120, 80, 0, 120
|
|
53
|
+
close
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Commands are serialized as `M`, `L`, `Q`, `C`, `H`, `V`, `A`, and `Z`. The
|
|
58
|
+
Canvas2D renderer draws `arc_to` as an SVG-style elliptical arc, and the line
|
|
59
|
+
fallback/snapshot renderer flattens curves and arcs to line segments.
|
|
60
|
+
`detail` controls the default curve/arc subdivision count. When `tolerance` is
|
|
61
|
+
provided, quadratic and cubic curves use adaptive flattening instead. `max_segments`
|
|
62
|
+
caps the number of flattened line segments per path; the DSL raises when the
|
|
63
|
+
estimated flattened path would exceed that budget.
|
|
64
|
+
|
|
65
|
+
## Style And Transform
|
|
66
|
+
|
|
67
|
+
Inside a shape block, these methods target the shape rather than the layer:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
fill "#f472b6"
|
|
71
|
+
stroke 2
|
|
72
|
+
stroke width: 3, color: "#ffffff"
|
|
73
|
+
blend :add
|
|
74
|
+
opacity 0.75
|
|
75
|
+
|
|
76
|
+
translate x: 100, y: 40
|
|
77
|
+
translate 100, 40
|
|
78
|
+
rotate 30
|
|
79
|
+
scale 1.2
|
|
80
|
+
scale x: 1.4, y: 0.8
|
|
81
|
+
origin x: 0, y: 0
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The transform order is origin adjustment, scale, rotate, then translate.
|
|
85
|
+
|
|
86
|
+
## Groups
|
|
87
|
+
|
|
88
|
+
Groups are flattened into child primitives during DSL evaluation. Child shapes
|
|
89
|
+
inherit style values they do not set themselves, and group opacity is multiplied
|
|
90
|
+
with child opacity.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
group :spinner do
|
|
94
|
+
translate x: 0, y: 80
|
|
95
|
+
rotate 15
|
|
96
|
+
scale 1.2
|
|
97
|
+
opacity 0.75
|
|
98
|
+
stroke width: 2, color: "#38bdf8"
|
|
99
|
+
|
|
100
|
+
12.times do |index|
|
|
101
|
+
rect width: 12, height: 80 do
|
|
102
|
+
translate x: 0, y: 160
|
|
103
|
+
rotate index * 30
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Mapping
|
|
110
|
+
|
|
111
|
+
Mappings declared inside a shape block are scoped to that shape:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
circle :ring, radius: 120 do
|
|
115
|
+
map bass, to: :radius, range: 80..240
|
|
116
|
+
map beat_pulse, to: :scale, range: 1.0..1.4
|
|
117
|
+
map high, to: :opacity, range: 0.2..1.0
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Transform aliases:
|
|
122
|
+
|
|
123
|
+
- `:translate_x` -> `transform.translate.x`
|
|
124
|
+
- `:translate_y` -> `transform.translate.y`
|
|
125
|
+
- `:rotate` and `:rotation` -> `transform.rotate`
|
|
126
|
+
- `:scale` -> `transform.scale`
|
|
127
|
+
- `:scale_x` -> `transform.scale.x`
|
|
128
|
+
- `:scale_y` -> `transform.scale.y`
|
|
129
|
+
- `:origin_x` -> `transform.origin.x`
|
|
130
|
+
- `:origin_y` -> `transform.origin.y`
|
|
131
|
+
|
|
132
|
+
You can also map to a named shape from the layer scope:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
rect :panel, width: 360, height: 160
|
|
136
|
+
map bass, to: shape(:panel).rotate, range: -12..12
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Mappings inside a static `custom_shape` block are applied to each primitive
|
|
140
|
+
generated by that custom shape:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
custom_shape :badge, radius: 120 do
|
|
144
|
+
fill "#22d3ee"
|
|
145
|
+
map beat_pulse, to: :scale, range: 0.8..1.2
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Custom Shapes
|
|
150
|
+
|
|
151
|
+
Custom shapes are registered in Ruby and expanded into regular shape
|
|
152
|
+
primitives before the browser receives the scene.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
class DiamondShape
|
|
156
|
+
include Vizcore::Shape
|
|
157
|
+
|
|
158
|
+
param :radius, default: 100, min: 10, max: 400
|
|
159
|
+
|
|
160
|
+
def draw(ctx)
|
|
161
|
+
radius = ctx.param(:radius)
|
|
162
|
+
|
|
163
|
+
ctx.polygon points: [
|
|
164
|
+
[0, radius],
|
|
165
|
+
[radius, 0],
|
|
166
|
+
[0, -radius],
|
|
167
|
+
[-radius, 0]
|
|
168
|
+
]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
Vizcore.register_shape :diamond, DiamondShape
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Use the registered shape from a layer:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
layer :generated_logo do
|
|
179
|
+
custom_shape :diamond, radius: 140 do
|
|
180
|
+
rotate 45
|
|
181
|
+
stroke width: 2, color: "#ffffff"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Block registration is supported for small generators:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
Vizcore.register_shape :spark do |ctx|
|
|
190
|
+
ctx.star points: 5, radius: ctx.param(:radius, 80), inner_radius: 32
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`draw(ctx)` may return a primitive hash, an array of primitive hashes, or build
|
|
195
|
+
primitives with `ctx.draw`, `ctx.circle`, `ctx.rect`, `ctx.path`, and the other
|
|
196
|
+
shape methods. Values declared with `param` are validated against their
|
|
197
|
+
`min`/`max` metadata before `draw(ctx)` runs, and dynamic custom shape
|
|
198
|
+
descriptors include the same schema for editor tooling.
|
|
199
|
+
|
|
200
|
+
By default, custom shapes are expanded when the DSL is evaluated. Use
|
|
201
|
+
`dynamic: true` when the generator needs mapped params, `ctx.time`, `ctx.frame`,
|
|
202
|
+
or `ctx.audio` at runtime:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
custom_shape :orbit, count: 32, radius: 260, dynamic: true do
|
|
206
|
+
fill "#22d3ee"
|
|
207
|
+
map bass, to: :radius, range: 120..280
|
|
208
|
+
map beat_pulse, to: :scale, range: 0.8..1.2
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Inside a dynamic custom shape block, mappings to custom params such as `:radius`
|
|
213
|
+
are applied before `draw(ctx)` runs. Transform aliases such as `:scale` and
|
|
214
|
+
`:rotate` are applied to the generated primitives after expansion.
|
|
215
|
+
|
|
216
|
+
Use `static: true` for generators that are independent of runtime context. Static
|
|
217
|
+
custom shapes with identical renderer, params, layer context, palette, and
|
|
218
|
+
resolution reuse a cached expansion, while each call still receives its own copy
|
|
219
|
+
of the generated primitives.
|
|
220
|
+
|
|
221
|
+
## Coordinates
|
|
222
|
+
|
|
223
|
+
New shape schema layers use center-origin logical coordinates by default:
|
|
224
|
+
|
|
225
|
+
- `x` increases to the right.
|
|
226
|
+
- `y` increases upward.
|
|
227
|
+
- `x: 0, y: 0` is the center of the canvas.
|
|
228
|
+
|
|
229
|
+
Legacy `circle` and `line` layers without `shape_schema_version: 2` keep the old
|
|
230
|
+
coordinate heuristic for compatibility with existing examples.
|
|
231
|
+
|
|
232
|
+
Set `units :ndc` on the layer to use normalized device coordinates directly.
|
|
233
|
+
|
|
234
|
+
## Validation
|
|
235
|
+
|
|
236
|
+
The DSL raises early for malformed primitives:
|
|
237
|
+
|
|
238
|
+
- duplicate shape IDs within a layer
|
|
239
|
+
- negative `radius`, `inner_radius`, `width`, `height`, or `stroke_width`
|
|
240
|
+
- `polygon` with fewer than 3 points
|
|
241
|
+
- `polyline` with fewer than 2 points
|
|
242
|
+
- `path` without commands
|
|
243
|
+
- `path` whose estimated flattened segment count exceeds `max_segments`
|
|
244
|
+
- unknown `custom_shape`
|
|
245
|
+
- custom shapes that return unsupported primitive kinds
|
|
246
|
+
|
|
247
|
+
`vizcore validate` also emits warnings for shape payloads that are renderable but
|
|
248
|
+
likely surprising: unsupported primitive kinds in raw `params[:shapes]`, fills
|
|
249
|
+
that line fallback will ignore, opacity values outside `0..1`, and zero or very
|
|
250
|
+
large scale values that will collapse or clamp.
|
|
251
|
+
|
|
252
|
+
## Renderer Notes
|
|
253
|
+
|
|
254
|
+
The browser renderer uses a Canvas2D shape backend composited through the
|
|
255
|
+
existing WebGL layer pipeline. It supports fill, stroke color/width, opacity,
|
|
256
|
+
dash, line caps/joins, transforms, and logical/ndc coordinates for the shape
|
|
257
|
+
schema.
|
|
258
|
+
|
|
259
|
+
If Canvas2D is unavailable, the browser falls back to the existing line renderer.
|
|
260
|
+
That fallback ignores fill and only approximates stroke geometry. The software
|
|
261
|
+
snapshot renderer also uses line flattening.
|
|
262
|
+
|
|
263
|
+
The browser HUD includes local shape editor controls for resolved shape layers:
|
|
264
|
+
primitive kind, translate, rotate, scale, opacity, fill, stroke color, and stroke
|
|
265
|
+
width can be adjusted in the running view without mutating the source DSL file.
|
|
266
|
+
Dynamic custom shape params with `param` metadata are also exposed as HUD
|
|
267
|
+
controls; changing one sends a runtime override to the server so the Ruby custom
|
|
268
|
+
shape is re-expanded on the next frame. The HUD also lists valid mapping target
|
|
269
|
+
paths for layer params, primitive params/transforms, and custom shape params.
|
data/frontend/index.html
CHANGED
|
@@ -409,6 +409,29 @@
|
|
|
409
409
|
text-align: right;
|
|
410
410
|
}
|
|
411
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
|
+
|
|
412
435
|
@media (max-width: 520px) {
|
|
413
436
|
.hud {
|
|
414
437
|
right: 1rem;
|
|
@@ -436,6 +459,7 @@
|
|
|
436
459
|
<p id="ws-status">WebSocket: connecting...</p>
|
|
437
460
|
<p id="scene-status">Scene: unknown</p>
|
|
438
461
|
<p id="transition-status">Transition: none</p>
|
|
462
|
+
<p id="runtime-error-status">Runtime: ok</p>
|
|
439
463
|
<p id="frame-status">Amplitude: 0.0000</p>
|
|
440
464
|
<p id="bpm-status" class="is-accent">BPM: --</p>
|
|
441
465
|
<p id="beat-status">Beat: off | Count: 0</p>
|
|
@@ -444,7 +468,7 @@
|
|
|
444
468
|
<button id="freeze-toggle" type="button" aria-pressed="false">Freeze</button>
|
|
445
469
|
<p id="live-control-status" class="live-controls__status">Live: output</p>
|
|
446
470
|
</div>
|
|
447
|
-
<p id="performance-monitor" class="performance-monitor">Perf: -- FPS | Frame -- | WS -- | RTT -- | Clock -- | Drop 0 | Audio -- | Shader -- | Reconnect 0</p>
|
|
471
|
+
<p id="performance-monitor" class="performance-monitor">Perf: -- FPS | Frame -- | WS -- | RTT -- | Probe max -- | Probe p95 -- | Clock -- | Drop 0 | BDrop 0 | WSLag --f | Audio -- | Capture -- | Analyze -- | Build -- | Shader -- | DPR -- | MaxTex -- | DrawBuf -- | floatColorBuffer no | textureFloat no | Safe off | Backpressure -- | Reconnect 0</p>
|
|
448
472
|
<div class="audio-inspector" aria-label="Audio inspector">
|
|
449
473
|
<div class="audio-inspector__header">
|
|
450
474
|
<span>Audio</span>
|
|
@@ -478,6 +502,7 @@
|
|
|
478
502
|
<div id="fft-preview" class="fft-preview" aria-label="FFT preview"></div>
|
|
479
503
|
</div>
|
|
480
504
|
<p id="audio-source-status">Audio Source: unknown</p>
|
|
505
|
+
<p id="audio-health-status">Audio Health: unknown</p>
|
|
481
506
|
<p id="audio-track-status">Track: none</p>
|
|
482
507
|
<p id="audio-playback-status">Playback: unavailable</p>
|
|
483
508
|
<div class="reactivity-controls" aria-label="Visual reactivity controls">
|
|
@@ -507,6 +532,9 @@
|
|
|
507
532
|
</div>
|
|
508
533
|
</div>
|
|
509
534
|
<div id="shader-param-controls" class="shader-param-controls" hidden aria-label="Shader parameter controls"></div>
|
|
535
|
+
<div id="shape-editor-controls" class="shader-param-controls" hidden aria-label="Shape editor controls"></div>
|
|
536
|
+
<div id="custom-shape-param-controls" class="shader-param-controls" hidden aria-label="Custom shape parameter controls"></div>
|
|
537
|
+
<div id="mapping-target-selector" class="shader-param-controls" hidden aria-label="Mapping target selector"></div>
|
|
510
538
|
<div id="scene-switcher" class="scene-switcher" hidden aria-label="Scene switcher"></div>
|
|
511
539
|
<button id="audio-toggle" type="button" hidden>Play Audio</button>
|
|
512
540
|
</section>
|
|
@@ -523,7 +551,12 @@
|
|
|
523
551
|
window.__vizcoreMainStarted = false;
|
|
524
552
|
(function () {
|
|
525
553
|
var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
526
|
-
var
|
|
554
|
+
var pathname = String(window.location.pathname || "");
|
|
555
|
+
var search = String(window.location.search || "");
|
|
556
|
+
var params = new URLSearchParams(search);
|
|
557
|
+
var mode = String(params.get("mode") || "").toLowerCase();
|
|
558
|
+
var role = mode === "projector" || pathname.indexOf("/projector") === 0 ? "projector" : (mode === "monitor" ? "monitor" : "control");
|
|
559
|
+
var websocketUrl = protocol + "//" + window.location.host + "/ws?role=" + encodeURIComponent(role);
|
|
527
560
|
var fallbackTimer = window.setTimeout(function () {
|
|
528
561
|
if (window.__vizcoreMainStarted) return;
|
|
529
562
|
|
|
@@ -533,6 +566,7 @@
|
|
|
533
566
|
var bpmStatus = document.getElementById("bpm-status");
|
|
534
567
|
var beatStatus = document.getElementById("beat-status");
|
|
535
568
|
var audioSourceStatus = document.getElementById("audio-source-status");
|
|
569
|
+
var audioHealthStatus = document.getElementById("audio-health-status");
|
|
536
570
|
var peakStatus = document.getElementById("inspector-peak");
|
|
537
571
|
var ampValue = document.getElementById("inspector-amplitude-value");
|
|
538
572
|
var ampFill = document.getElementById("inspector-amplitude-fill");
|
|
@@ -556,6 +590,20 @@
|
|
|
556
590
|
if (runtime && audioSourceStatus) {
|
|
557
591
|
audioSourceStatus.textContent = "Audio Source: " + runtime.audio_source;
|
|
558
592
|
}
|
|
593
|
+
if (runtime && audioHealthStatus) {
|
|
594
|
+
var input = runtime.input || {};
|
|
595
|
+
var ringBuffer = input.ring_buffer || {};
|
|
596
|
+
var sampleRate = Number(input.sample_rate || 0);
|
|
597
|
+
var requestedRate = Number(input.requested_sample_rate || 0);
|
|
598
|
+
var frameSize = Number(input.frame_size || 0);
|
|
599
|
+
var overrun = Number(ringBuffer.overrun_count || 0);
|
|
600
|
+
var underrun = Number(ringBuffer.underrun_count || 0);
|
|
601
|
+
var mismatch = input.sample_rate_mismatch ? " mismatched" : "";
|
|
602
|
+
var rateText = "Rate: " + (sampleRate > 0 ? sampleRate : "--") + "Hz" + (requestedRate > 0 && requestedRate !== sampleRate ? " (req " + requestedRate + "Hz)" : "");
|
|
603
|
+
var frameText = "Frame: " + (frameSize > 0 ? frameSize : "--");
|
|
604
|
+
var healthText = "Ring: " + overrun + "/" + underrun + " over/under";
|
|
605
|
+
audioHealthStatus.textContent = "Audio Health: " + rateText + ", " + frameText + mismatch + ", " + healthText;
|
|
606
|
+
}
|
|
559
607
|
})
|
|
560
608
|
.catch(function () {});
|
|
561
609
|
|
|
@@ -6,14 +6,23 @@ export const buildAudioInspectorState = (audio, fftBins = DEFAULT_FFT_BINS) => {
|
|
|
6
6
|
result[key] = clamp01(audio?.bands?.[key]);
|
|
7
7
|
return result;
|
|
8
8
|
}, {});
|
|
9
|
+
const bandPeaks = BAND_KEYS.reduce((result, key) => {
|
|
10
|
+
result[key] = clamp01(audio?.band_peaks?.[key]);
|
|
11
|
+
return result;
|
|
12
|
+
}, {});
|
|
9
13
|
|
|
10
14
|
return {
|
|
11
15
|
amplitude: clamp01(audio?.amplitude),
|
|
12
16
|
bands,
|
|
17
|
+
bandPeaks,
|
|
13
18
|
fft: normalizeFft(audio?.fft, fftBins),
|
|
14
19
|
bpm: Number(audio?.bpm || 0),
|
|
15
20
|
beat: !!audio?.beat,
|
|
16
21
|
beatPulse: clamp01(audio?.beat_pulse),
|
|
22
|
+
beatPhase: clamp01(audio?.beat_phase),
|
|
23
|
+
barPhase: clamp01(audio?.bar_phase),
|
|
24
|
+
barCount: Math.max(0, Number(audio?.bar_count || 0) || 0),
|
|
25
|
+
phraseCount: Math.max(0, Number(audio?.phrase_count || 0) || 0),
|
|
17
26
|
peakFrequency: Math.max(0, Number(audio?.peak_frequency || 0) || 0),
|
|
18
27
|
};
|
|
19
28
|
};
|
|
@@ -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);
|