vizcore 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +66 -648
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/index.html +2 -1
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/frontend/index.html +50 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +703 -45
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +401 -11
- data/frontend/src/renderer/layer-manager.js +490 -75
- data/frontend/src/runtime-control-preset.js +44 -0
- data/frontend/src/scene-patches.js +159 -0
- data/frontend/src/shader-error-overlay.js +1 -0
- data/frontend/src/shape-editor-controls.js +157 -0
- data/frontend/src/visuals/geometry.js +425 -27
- data/frontend/src/visuals/image-renderer.js +19 -0
- data/frontend/src/visuals/particle-system.js +10 -0
- data/frontend/src/visuals/shape-renderer.js +488 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -0
- data/frontend/src/visuals/svg-arc.js +104 -0
- data/frontend/src/visuals/text-renderer.js +13 -0
- data/frontend/src/websocket-client.js +6 -0
- data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
- data/lib/vizcore/analysis/feature_recorder.rb +117 -7
- data/lib/vizcore/analysis/feature_replay.rb +48 -9
- data/lib/vizcore/analysis/pipeline.rb +258 -9
- data/lib/vizcore/analysis/tap_tempo.rb +17 -2
- data/lib/vizcore/audio/calibration.rb +156 -0
- data/lib/vizcore/audio/file_input.rb +28 -0
- data/lib/vizcore/audio/input_manager.rb +36 -1
- data/lib/vizcore/audio/midi_input.rb +5 -0
- data/lib/vizcore/audio/ring_buffer.rb +22 -0
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/dsl_reference.rb +65 -9
- data/lib/vizcore/cli/plugin_checker.rb +93 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
- data/lib/vizcore/cli/scene_inspector.rb +35 -1
- data/lib/vizcore/cli/scene_validator.rb +573 -33
- data/lib/vizcore/cli/shader_template.rb +7 -2
- data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
- data/lib/vizcore/cli.rb +268 -15
- data/lib/vizcore/config.rb +40 -3
- data/lib/vizcore/control_preset.rb +29 -0
- data/lib/vizcore/deep_copy.rb +21 -0
- data/lib/vizcore/dsl/color_helpers.rb +155 -0
- data/lib/vizcore/dsl/engine.rb +219 -23
- data/lib/vizcore/dsl/layer_builder.rb +1072 -21
- data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
- data/lib/vizcore/dsl/layout_helpers.rb +290 -0
- data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +549 -13
- data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
- data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
- data/lib/vizcore/dsl/reaction_builder.rb +1 -0
- data/lib/vizcore/dsl/scene_builder.rb +83 -13
- data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
- data/lib/vizcore/dsl/style_builder.rb +3 -0
- data/lib/vizcore/dsl/timeline_builder.rb +91 -8
- data/lib/vizcore/dsl/transition_controller.rb +157 -18
- data/lib/vizcore/dsl.rb +2 -0
- data/lib/vizcore/layer_catalog.rb +5 -2
- data/lib/vizcore/plugin_asset_policy.rb +55 -0
- data/lib/vizcore/project_manifest.rb +12 -2
- data/lib/vizcore/renderer/render_sequence.rb +104 -13
- data/lib/vizcore/renderer/scene_frame_source.rb +190 -12
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +641 -23
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +513 -18
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +697 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +742 -0
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +34 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +154 -4
- metadata +29 -3
|
@@ -1,24 +1,185 @@
|
|
|
1
|
+
const finiteFloat = (value) => {
|
|
2
|
+
const numeric = Number(value);
|
|
3
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const clamp01 = (value) => {
|
|
7
|
+
if (!Number.isFinite(value)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return Math.min(1, Math.max(0, value));
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const parseHexColor = (value) => {
|
|
14
|
+
const raw = String(value || "").trim();
|
|
15
|
+
if (!raw.startsWith("#")) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const rawHex = raw.slice(1);
|
|
20
|
+
const isShort = rawHex.length === 3 || rawHex.length === 4;
|
|
21
|
+
const isLong = rawHex.length === 6 || rawHex.length === 8;
|
|
22
|
+
if (!/^[0-9a-fA-F]+$/.test(rawHex) || (!isShort && !isLong)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const expanded = isShort
|
|
27
|
+
? rawHex.split("").map((entry) => `${entry}${entry}`).join("")
|
|
28
|
+
: rawHex;
|
|
29
|
+
|
|
30
|
+
const channels = [];
|
|
31
|
+
for (let index = 0; index < expanded.length; index += 2) {
|
|
32
|
+
const channel = Number.parseInt(expanded.slice(index, index + 2), 16);
|
|
33
|
+
channels.push(clamp01(channel / 255));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return channels;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const normalizeColorChannels = (value, hasAlpha = false) => {
|
|
40
|
+
const values = Array.from(value || []);
|
|
41
|
+
if (values.length < 3 || values.length > 4) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rgbValues = values.slice(0, 3);
|
|
46
|
+
const shouldScaleRgbBy255 = rgbValues.some((channel) => channel > 1);
|
|
47
|
+
const rgb = rgbValues.map((channel) => (shouldScaleRgbBy255 ? clamp01(channel / 255) : clamp01(channel)));
|
|
48
|
+
const alpha = values.length === 4 ? clamp01(values[3] > 1 ? values[3] / 255 : values[3]) : null;
|
|
49
|
+
if (rgb.includes(null) || (values.length === 4 && alpha === null)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return hasAlpha ? [...rgb, alpha] : rgb;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const normalizeLiveControlColor = (value) => {
|
|
57
|
+
if (value == null) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
const channels = Array.from(value)
|
|
63
|
+
.slice(0, 4)
|
|
64
|
+
.map((channel) => Number(channel));
|
|
65
|
+
if (channels.length < 3 || channels.some((channel) => !Number.isFinite(channel))) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const rgbValues = channels.slice(0, 3);
|
|
70
|
+
const alpha = channels.length === 4 ? clamp01(channels[3] > 1 ? channels[3] / 255 : channels[3]) : null;
|
|
71
|
+
if (channels.length === 4 && alpha === null) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const shouldScaleRgbBy255 = rgbValues.some((channel) => channel > 1);
|
|
76
|
+
const normalizedRgb = rgbValues.map((channel) => (shouldScaleRgbBy255 ? clamp01(channel / 255) : clamp01(channel)));
|
|
77
|
+
return channels.length === 4 ? [...normalizedRgb, alpha] : normalizedRgb;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const parsed = parseHexColor(value);
|
|
81
|
+
if (!parsed) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return normalizeColorChannels(parsed, parsed.length === 4);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const createLiveControlEntry = (enabled = false, fade = undefined, release = undefined, color = undefined) => {
|
|
89
|
+
return compactLiveControlEntry({
|
|
90
|
+
enabled: !!enabled,
|
|
91
|
+
fade: finiteFloat(fade),
|
|
92
|
+
release: finiteFloat(release),
|
|
93
|
+
color: normalizeLiveControlColor(color),
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const compactLiveControlEntry = (entry) => {
|
|
98
|
+
const output = { ...entry };
|
|
99
|
+
if (!Object.prototype.hasOwnProperty.call(output, "fade")) {
|
|
100
|
+
return output;
|
|
101
|
+
}
|
|
102
|
+
if (output.fade === null || output.fade === undefined) {
|
|
103
|
+
delete output.fade;
|
|
104
|
+
}
|
|
105
|
+
if (output.release === null || output.release === undefined) {
|
|
106
|
+
delete output.release;
|
|
107
|
+
}
|
|
108
|
+
if (output.color === null || output.color === undefined) {
|
|
109
|
+
delete output.color;
|
|
110
|
+
}
|
|
111
|
+
return output;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const normalizeLiveControlState = (value) => {
|
|
115
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
116
|
+
if (Object.prototype.hasOwnProperty.call(value, "value")) {
|
|
117
|
+
return compactLiveControlEntry(createLiveControlEntry(
|
|
118
|
+
!!value.value,
|
|
119
|
+
value.fade,
|
|
120
|
+
value.release,
|
|
121
|
+
value.color
|
|
122
|
+
));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return compactLiveControlEntry(createLiveControlEntry(
|
|
126
|
+
Object.prototype.hasOwnProperty.call(value, "enabled") ? !!value.enabled : false,
|
|
127
|
+
value.fade,
|
|
128
|
+
value.release,
|
|
129
|
+
value.color
|
|
130
|
+
));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return compactLiveControlEntry(createLiveControlEntry(!!value));
|
|
134
|
+
};
|
|
135
|
+
|
|
1
136
|
export const createLiveControlState = () => ({
|
|
2
|
-
blackout: false,
|
|
3
|
-
freeze: false,
|
|
137
|
+
blackout: createLiveControlEntry(false),
|
|
138
|
+
freeze: createLiveControlEntry(false),
|
|
4
139
|
});
|
|
5
140
|
|
|
141
|
+
export const isLiveControlEnabled = (state) => {
|
|
142
|
+
if (!state) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof state === "object") {
|
|
147
|
+
return !!state.enabled;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return !!state;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const isLiveControlActive = (state) => {
|
|
154
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) {
|
|
155
|
+
return isLiveControlEnabled(state);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return !!state.enabled;
|
|
159
|
+
};
|
|
160
|
+
|
|
6
161
|
export const toggleLiveControl = (state, key) => {
|
|
7
162
|
const control = String(key || "");
|
|
8
163
|
if (control !== "blackout" && control !== "freeze") {
|
|
9
164
|
return { ...state };
|
|
10
165
|
}
|
|
11
166
|
|
|
167
|
+
const current = normalizeLiveControlState(state?.[control]);
|
|
12
168
|
return {
|
|
13
169
|
...state,
|
|
14
|
-
[control]:
|
|
170
|
+
[control]: {
|
|
171
|
+
...current,
|
|
172
|
+
enabled: !current.enabled,
|
|
173
|
+
},
|
|
15
174
|
};
|
|
16
175
|
};
|
|
17
176
|
|
|
177
|
+
export const normalizeLiveControlPayload = (state) => normalizeLiveControlState(state);
|
|
178
|
+
|
|
18
179
|
export const liveControlStatusText = (state) => {
|
|
19
180
|
const values = [];
|
|
20
|
-
if (state?.blackout) values.push("Blackout");
|
|
21
|
-
if (state?.freeze) values.push("Freeze");
|
|
181
|
+
if (isLiveControlActive(state?.blackout)) values.push("Blackout");
|
|
182
|
+
if (isLiveControlActive(state?.freeze)) values.push("Freeze");
|
|
22
183
|
return values.length ? `Live: ${values.join(" + ")}` : "Live: output";
|
|
23
184
|
};
|
|
24
185
|
|
|
@@ -107,17 +268,59 @@ const normalizeKeyboardAction = (action) => {
|
|
|
107
268
|
const type = String(action?.type || "").trim();
|
|
108
269
|
if (type === "switch_scene") {
|
|
109
270
|
const scene = String(action?.scene || "").trim();
|
|
110
|
-
|
|
271
|
+
if (!scene) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const effect = normalizeTransitionEffect(action?.effect);
|
|
275
|
+
return effect ? { type, scene, effect } : { type, scene };
|
|
111
276
|
}
|
|
112
277
|
|
|
113
278
|
if (type === "live_control") {
|
|
114
279
|
const control = String(action?.control || "").trim();
|
|
115
|
-
|
|
280
|
+
if (control !== "blackout" && control !== "freeze") {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const payload = { type, control };
|
|
285
|
+
if (Object.prototype.hasOwnProperty.call(action, "value")) {
|
|
286
|
+
payload.value = action.value;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const fade = finiteFloat(action?.fade);
|
|
290
|
+
if (fade !== null) {
|
|
291
|
+
payload.fade = fade;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const release = finiteFloat(action?.release);
|
|
295
|
+
if (release !== null) {
|
|
296
|
+
payload.release = release;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const color = normalizeLiveControlColor(action?.color);
|
|
300
|
+
if (color !== null) {
|
|
301
|
+
payload.color = color;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return payload;
|
|
116
305
|
}
|
|
117
306
|
|
|
118
307
|
return null;
|
|
119
308
|
};
|
|
120
309
|
|
|
310
|
+
const normalizeTransitionEffect = (effect) => {
|
|
311
|
+
if (!effect) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
if (typeof effect === "string" || typeof effect === "number" || typeof effect === "symbol") {
|
|
315
|
+
return { name: String(effect) };
|
|
316
|
+
}
|
|
317
|
+
if (typeof effect !== "object" || Array.isArray(effect)) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return effect;
|
|
322
|
+
};
|
|
323
|
+
|
|
121
324
|
export const isEditableShortcutTarget = (target) => {
|
|
122
325
|
if (!target) {
|
|
123
326
|
return false;
|
|
@@ -129,3 +332,12 @@ export const isEditableShortcutTarget = (target) => {
|
|
|
129
332
|
|| tagName === "select"
|
|
130
333
|
|| target.isContentEditable === true;
|
|
131
334
|
};
|
|
335
|
+
|
|
336
|
+
const clamp = (value, min, max) => {
|
|
337
|
+
const numeric = Number(value);
|
|
338
|
+
if (!Number.isFinite(numeric)) {
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return Math.min(max, Math.max(min, numeric));
|
|
343
|
+
};
|