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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +66 -648
  3. data/docs/assets/playground-worker.js +373 -0
  4. data/docs/assets/playground.css +440 -0
  5. data/docs/assets/playground.js +652 -0
  6. data/docs/index.html +2 -1
  7. data/docs/playground.html +81 -0
  8. data/docs/shape_dsl.md +269 -0
  9. data/frontend/index.html +50 -2
  10. data/frontend/src/audio-inspector.js +9 -0
  11. data/frontend/src/custom-shape-param-controls.js +106 -0
  12. data/frontend/src/live-controls.js +219 -7
  13. data/frontend/src/main.js +703 -45
  14. data/frontend/src/mapping-target-selector.js +109 -0
  15. data/frontend/src/midi-learn.js +22 -2
  16. data/frontend/src/performance-monitor.js +137 -1
  17. data/frontend/src/renderer/engine.js +401 -11
  18. data/frontend/src/renderer/layer-manager.js +490 -75
  19. data/frontend/src/runtime-control-preset.js +44 -0
  20. data/frontend/src/scene-patches.js +159 -0
  21. data/frontend/src/shader-error-overlay.js +1 -0
  22. data/frontend/src/shape-editor-controls.js +157 -0
  23. data/frontend/src/visuals/geometry.js +425 -27
  24. data/frontend/src/visuals/image-renderer.js +19 -0
  25. data/frontend/src/visuals/particle-system.js +10 -0
  26. data/frontend/src/visuals/shape-renderer.js +488 -0
  27. data/frontend/src/visuals/spectrogram-renderer.js +14 -0
  28. data/frontend/src/visuals/svg-arc.js +104 -0
  29. data/frontend/src/visuals/text-renderer.js +13 -0
  30. data/frontend/src/websocket-client.js +6 -0
  31. data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
  32. data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
  33. data/lib/vizcore/analysis/feature_recorder.rb +117 -7
  34. data/lib/vizcore/analysis/feature_replay.rb +48 -9
  35. data/lib/vizcore/analysis/pipeline.rb +258 -9
  36. data/lib/vizcore/analysis/tap_tempo.rb +17 -2
  37. data/lib/vizcore/audio/calibration.rb +156 -0
  38. data/lib/vizcore/audio/file_input.rb +28 -0
  39. data/lib/vizcore/audio/input_manager.rb +36 -1
  40. data/lib/vizcore/audio/midi_input.rb +5 -0
  41. data/lib/vizcore/audio/ring_buffer.rb +22 -0
  42. data/lib/vizcore/audio.rb +1 -0
  43. data/lib/vizcore/cli/dsl_reference.rb +65 -9
  44. data/lib/vizcore/cli/plugin_checker.rb +93 -0
  45. data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
  46. data/lib/vizcore/cli/scene_inspector.rb +35 -1
  47. data/lib/vizcore/cli/scene_validator.rb +573 -33
  48. data/lib/vizcore/cli/shader_template.rb +7 -2
  49. data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
  50. data/lib/vizcore/cli.rb +268 -15
  51. data/lib/vizcore/config.rb +40 -3
  52. data/lib/vizcore/control_preset.rb +29 -0
  53. data/lib/vizcore/deep_copy.rb +21 -0
  54. data/lib/vizcore/dsl/color_helpers.rb +155 -0
  55. data/lib/vizcore/dsl/engine.rb +219 -23
  56. data/lib/vizcore/dsl/layer_builder.rb +1072 -21
  57. data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
  58. data/lib/vizcore/dsl/layout_helpers.rb +290 -0
  59. data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
  60. data/lib/vizcore/dsl/mapping_resolver.rb +549 -13
  61. data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
  62. data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
  63. data/lib/vizcore/dsl/reaction_builder.rb +1 -0
  64. data/lib/vizcore/dsl/scene_builder.rb +83 -13
  65. data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
  66. data/lib/vizcore/dsl/style_builder.rb +3 -0
  67. data/lib/vizcore/dsl/timeline_builder.rb +91 -8
  68. data/lib/vizcore/dsl/transition_controller.rb +157 -18
  69. data/lib/vizcore/dsl.rb +2 -0
  70. data/lib/vizcore/layer_catalog.rb +5 -2
  71. data/lib/vizcore/plugin_asset_policy.rb +55 -0
  72. data/lib/vizcore/project_manifest.rb +12 -2
  73. data/lib/vizcore/renderer/render_sequence.rb +104 -13
  74. data/lib/vizcore/renderer/scene_frame_source.rb +190 -12
  75. data/lib/vizcore/renderer/scene_serializer.rb +38 -0
  76. data/lib/vizcore/renderer/snapshot.rb +4 -3
  77. data/lib/vizcore/renderer/snapshot_renderer.rb +641 -23
  78. data/lib/vizcore/scene_trust.rb +31 -0
  79. data/lib/vizcore/server/frame_broadcaster.rb +513 -18
  80. data/lib/vizcore/server/rack_app.rb +151 -4
  81. data/lib/vizcore/server/runner.rb +697 -82
  82. data/lib/vizcore/server/websocket_handler.rb +236 -14
  83. data/lib/vizcore/server.rb +21 -0
  84. data/lib/vizcore/shape.rb +742 -0
  85. data/lib/vizcore/sync/osc_message.rb +66 -9
  86. data/lib/vizcore/version.rb +1 -1
  87. data/lib/vizcore.rb +34 -0
  88. data/scripts/browser_capture.mjs +31 -2
  89. data/sig/vizcore.rbs +154 -4
  90. metadata +29 -3
@@ -3,9 +3,53 @@ export const normalizeRuntimeControlPreset = (value) => {
3
3
  return {
4
4
  visualSettings: objectValue(input.visual_settings) || objectValue(input.visualSettings) || null,
5
5
  midiLearnBindings: objectValue(input.midi_learn_bindings) || objectValue(input.midiLearnBindings) || null,
6
+ sceneOverrides: normalizeSceneOverrides(
7
+ input.scene_overrides || input.sceneOverrides || null
8
+ ),
6
9
  };
7
10
  };
8
11
 
9
12
  const objectValue = (value) => {
10
13
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
11
14
  };
15
+
16
+ const normalizeSceneOverrides = (value) => {
17
+ if (!value || typeof value !== "object") {
18
+ return {};
19
+ }
20
+
21
+ const output = {};
22
+ for (const [rawSceneName, rawOverride] of Object.entries(value)) {
23
+ const sceneName = String(rawSceneName || "").trim();
24
+ if (!sceneName) {
25
+ continue;
26
+ }
27
+
28
+ const override = normalizeSceneOverride(rawOverride);
29
+ if (!override) {
30
+ continue;
31
+ }
32
+
33
+ output[sceneName] = override;
34
+ }
35
+
36
+ return output;
37
+ };
38
+
39
+ const normalizeSceneOverride = (value) => {
40
+ const input = value && typeof value === "object" && !Array.isArray(value) ? value : {};
41
+ const visualSettings = objectValue(input.visual_settings) || objectValue(input.visualSettings);
42
+ const midiLearnBindings = objectValue(input.midi_learn_bindings) || objectValue(input.midiLearnBindings);
43
+ if (!visualSettings && !midiLearnBindings) {
44
+ return null;
45
+ }
46
+
47
+ const output = {};
48
+ if (visualSettings) {
49
+ output.visualSettings = visualSettings;
50
+ }
51
+ if (midiLearnBindings) {
52
+ output.midiLearnBindings = midiLearnBindings;
53
+ }
54
+ return output;
55
+ };
@@ -0,0 +1,159 @@
1
+ const REMOVED_LAYER = Symbol("vizcore.scene.removed_layer");
2
+
3
+ const isObject = (value) => value !== null && typeof value === "object";
4
+
5
+ const clonePayload = (value) => {
6
+ if (!isObject(value) && !Array.isArray(value)) {
7
+ return value;
8
+ }
9
+
10
+ if (typeof structuredClone === "function") {
11
+ try {
12
+ return structuredClone(value);
13
+ } catch {
14
+ // Fall back to JSON clone below.
15
+ }
16
+ }
17
+
18
+ try {
19
+ return JSON.parse(JSON.stringify(value));
20
+ } catch {
21
+ return null;
22
+ }
23
+ };
24
+
25
+ const sceneVersion = (value) => {
26
+ const numeric = Number(value);
27
+ return Number.isFinite(numeric) ? numeric : null;
28
+ };
29
+
30
+ const normalizeIndex = (value) => {
31
+ const numeric = Number(value);
32
+ return Number.isInteger(numeric) && numeric >= 0 ? numeric : null;
33
+ };
34
+
35
+ export const applyScenePayload = (payload) => {
36
+ if (!isObject(payload)) {
37
+ return null;
38
+ }
39
+
40
+ const scene = clonePayload(payload);
41
+ if (!isObject(scene)) {
42
+ return null;
43
+ }
44
+
45
+ if (!Array.isArray(scene.layers)) {
46
+ scene.layers = [];
47
+ } else {
48
+ scene.layers = scene.layers.map((entry) => clonePayload(entry)).filter(Boolean);
49
+ }
50
+
51
+ return scene;
52
+ };
53
+
54
+ export const applyScenePatch = (currentScene, patch) => {
55
+ if (!isObject(currentScene) || !isObject(patch) || !Array.isArray(Array.isArray(patch.layers) ? patch.layers : null)) {
56
+ return null;
57
+ }
58
+
59
+ const currentName = String(currentScene.name || "");
60
+ if (String(patch.name || "") !== currentName) {
61
+ return null;
62
+ }
63
+
64
+ const currentVersion = sceneVersion(currentScene.version);
65
+ const patchVersion = sceneVersion(patch.version);
66
+ if (currentVersion !== null && patchVersion !== null && patchVersion !== currentVersion) {
67
+ return null;
68
+ }
69
+
70
+ const next = applyScenePayload(currentScene);
71
+ if (!isObject(next)) {
72
+ return null;
73
+ }
74
+
75
+ const layers = next.layers;
76
+ for (const entry of patch.layers) {
77
+ if (!isObject(entry)) {
78
+ continue;
79
+ }
80
+
81
+ const index = normalizeIndex(entry.index);
82
+ if (index === null) {
83
+ continue;
84
+ }
85
+
86
+ if (entry.remove) {
87
+ if (index < layers.length) {
88
+ layers[index] = REMOVED_LAYER;
89
+ }
90
+ continue;
91
+ }
92
+
93
+ if (Object.prototype.hasOwnProperty.call(entry, "layer")) {
94
+ if (!isObject(entry.layer)) {
95
+ continue;
96
+ }
97
+ while (layers.length <= index) {
98
+ layers.push(REMOVED_LAYER);
99
+ }
100
+ layers[index] = applyScenePayload(entry.layer);
101
+ continue;
102
+ }
103
+
104
+ if (!Object.prototype.hasOwnProperty.call(entry, "params")) {
105
+ continue;
106
+ }
107
+ if (!isObject(entry.params)) {
108
+ continue;
109
+ }
110
+
111
+ const layer = layers[index];
112
+ if (!isObject(layer) || layer === REMOVED_LAYER) {
113
+ continue;
114
+ }
115
+
116
+ const params = isObject(layer.params) ? clonePayload(layer.params) : {};
117
+ layers[index] = {
118
+ ...layer,
119
+ params: {
120
+ ...params,
121
+ ...clonePayload(entry.params)
122
+ }
123
+ };
124
+ }
125
+
126
+ next.version = patchVersion !== null ? patchVersion : next.version;
127
+ if (Object.prototype.hasOwnProperty.call(patch, "schema_version")) {
128
+ next.schema_version = patch.schema_version;
129
+ }
130
+ next.layers = layers.filter((layer) => layer !== REMOVED_LAYER);
131
+
132
+ return next;
133
+ };
134
+
135
+ export const resolveScenePayload = ({ incomingScene = null, currentScene = null, frameVersion = null } = {}) => {
136
+ if (!isObject(incomingScene) && !isObject(currentScene)) {
137
+ return null;
138
+ }
139
+
140
+ if (incomingScene.patch) {
141
+ if (!isObject(currentScene)) {
142
+ return null;
143
+ }
144
+
145
+ const expectedVersion = sceneVersion(frameVersion);
146
+ const currentVersion = sceneVersion(currentScene.version);
147
+ if (expectedVersion !== null && currentVersion !== null && expectedVersion !== currentVersion) {
148
+ return applyScenePayload(currentScene);
149
+ }
150
+
151
+ return applyScenePatch(currentScene, incomingScene) || applyScenePayload(currentScene);
152
+ }
153
+
154
+ if (!isObject(incomingScene)) {
155
+ return applyScenePayload(currentScene);
156
+ }
157
+
158
+ return applyScenePayload(incomingScene);
159
+ };
@@ -7,6 +7,7 @@ export const buildShaderErrorDetail = ({ layer, error, phase }) => {
7
7
  name,
8
8
  shader,
9
9
  phase: String(phase || "shader"),
10
+ event: "shader_failed",
10
11
  message: normalizeErrorMessage(error),
11
12
  };
12
13
  };
@@ -0,0 +1,157 @@
1
+ const SHAPE_KINDS = ["circle", "line", "rect", "polygon", "polyline", "path", "star"];
2
+
3
+ export const shapeEditorLayerKey = (layer, index) => `${index}:${String(layer?.name || "layer")}`;
4
+
5
+ export const shapeEditorEntries = (layers, overrides = {}) => {
6
+ const entries = [];
7
+ const layerList = Array.isArray(layers) ? layers : [];
8
+
9
+ layerList.forEach((layer, layerIndex) => {
10
+ const shapes = Array.isArray(layer?.params?.shapes) ? layer.params.shapes : [];
11
+ if (!shapes.length) return;
12
+
13
+ const layerKey = shapeEditorLayerKey(layer, layerIndex);
14
+ const layerName = String(layer?.name || `layer_${layerIndex + 1}`);
15
+ shapes.forEach((shape, shapeIndex) => {
16
+ const edited = applyShapePatch(shape, overrides?.[layerKey]?.[shapeIndex]);
17
+ const id = String(edited?.id || `shape_${shapeIndex + 1}`);
18
+ const kind = normalizeKind(edited?.kind || edited?.type);
19
+ entries.push({
20
+ key: `${layerKey}:shapes.${shapeIndex}`,
21
+ layerKey,
22
+ layerName,
23
+ shapeIndex,
24
+ shapeId: id,
25
+ label: `${layerName}.${id}`,
26
+ kind,
27
+ values: shapeEditorValues(edited, kind),
28
+ });
29
+ });
30
+ });
31
+
32
+ return entries;
33
+ };
34
+
35
+ export const applyShapeEditorOverrides = (layers, overrides = {}) => {
36
+ const layerList = Array.isArray(layers) ? layers : [];
37
+
38
+ return layerList.map((layer, layerIndex) => {
39
+ const shapes = Array.isArray(layer?.params?.shapes) ? layer.params.shapes : null;
40
+ if (!shapes) return layer;
41
+
42
+ const layerKey = shapeEditorLayerKey(layer, layerIndex);
43
+ const layerOverrides = overrides?.[layerKey];
44
+ if (!layerOverrides || typeof layerOverrides !== "object") return layer;
45
+
46
+ return {
47
+ ...layer,
48
+ params: {
49
+ ...(layer?.params || {}),
50
+ shapes: shapes.map((shape, shapeIndex) => applyShapePatch(shape, layerOverrides[shapeIndex])),
51
+ },
52
+ };
53
+ });
54
+ };
55
+
56
+ export const pruneShapeEditorOverrides = (overrides = {}, entries = []) => {
57
+ const validKeys = new Set(entries.map((entry) => `${entry.layerKey}:${entry.shapeIndex}`));
58
+ const next = {};
59
+
60
+ Object.entries(overrides || {}).forEach(([layerKey, layerOverrides]) => {
61
+ if (!layerOverrides || typeof layerOverrides !== "object") return;
62
+
63
+ Object.entries(layerOverrides).forEach(([shapeIndex, patch]) => {
64
+ if (!validKeys.has(`${layerKey}:${shapeIndex}`) || !patch || typeof patch !== "object") return;
65
+
66
+ next[layerKey] ||= {};
67
+ next[layerKey][shapeIndex] = patch;
68
+ });
69
+ });
70
+
71
+ return next;
72
+ };
73
+
74
+ export const normalizeShapeEditorPatch = (values = {}) => {
75
+ const kind = normalizeKind(values.kind);
76
+ const output = {
77
+ kind,
78
+ opacity: clamp(finiteNumber(values.opacity, 1), 0, 1),
79
+ stroke_width: Math.max(0, finiteNumber(values.strokeWidth ?? values.stroke_width, 1)),
80
+ transform: {
81
+ translate: {
82
+ x: finiteNumber(values.translateX, 0),
83
+ y: finiteNumber(values.translateY, 0),
84
+ },
85
+ rotate: finiteNumber(values.rotate, 0),
86
+ scale: {
87
+ x: clamp(finiteNumber(values.scaleX, 1), -8, 8),
88
+ y: clamp(finiteNumber(values.scaleY, 1), -8, 8),
89
+ },
90
+ },
91
+ };
92
+ if (values.fillEnabled !== false && validColor(values.fill)) output.fill = values.fill;
93
+ if (values.strokeColorEnabled !== false && validColor(values.strokeColor ?? values.stroke_color)) {
94
+ output.stroke_color = values.strokeColor ?? values.stroke_color;
95
+ }
96
+ return output;
97
+ };
98
+
99
+ const shapeEditorValues = (shape, kind) => {
100
+ const transform = shape?.transform || {};
101
+ const translate = vectorValue(transform.translate ?? shape?.translate, { x: 0, y: 0 });
102
+ const scale = vectorValue(transform.scale ?? shape?.scale, { x: 1, y: 1 });
103
+ return {
104
+ kind,
105
+ translateX: translate.x,
106
+ translateY: translate.y,
107
+ rotate: finiteNumber(transform.rotate ?? shape?.rotate ?? shape?.rotation, 0),
108
+ scaleX: scale.x,
109
+ scaleY: scale.y,
110
+ opacity: clamp(finiteNumber(shape?.opacity, 1), 0, 1),
111
+ fill: validColor(shape?.fill) ? shape.fill : "#000000",
112
+ fillEnabled: validColor(shape?.fill),
113
+ strokeColor: validColor(shape?.stroke_color ?? shape?.strokeColor) ? (shape.stroke_color ?? shape.strokeColor) : "#ffffff",
114
+ strokeColorEnabled: validColor(shape?.stroke_color ?? shape?.strokeColor),
115
+ strokeWidth: Math.max(0, finiteNumber(shape?.stroke_width ?? shape?.strokeWidth ?? shape?.stroke, 1)),
116
+ };
117
+ };
118
+
119
+ const applyShapePatch = (shape, patch) => {
120
+ if (!patch || typeof patch !== "object") return shape;
121
+
122
+ return {
123
+ ...(shape || {}),
124
+ ...patch,
125
+ transform: {
126
+ ...((shape || {}).transform || {}),
127
+ ...(patch.transform || {}),
128
+ },
129
+ };
130
+ };
131
+
132
+ const vectorValue = (value, fallback) => {
133
+ if (Array.isArray(value)) {
134
+ return { x: finiteNumber(value[0], fallback.x), y: finiteNumber(value[1], fallback.y) };
135
+ }
136
+
137
+ if (value && typeof value === "object") {
138
+ return { x: finiteNumber(value.x, fallback.x), y: finiteNumber(value.y, fallback.y) };
139
+ }
140
+
141
+ const numeric = finiteNumber(value, null);
142
+ return numeric === null ? fallback : { x: numeric, y: numeric };
143
+ };
144
+
145
+ const normalizeKind = (value) => {
146
+ const kind = String(value || "circle").trim().toLowerCase();
147
+ return SHAPE_KINDS.includes(kind) ? kind : "circle";
148
+ };
149
+
150
+ const validColor = (value) => /^#[0-9a-f]{6}$/i.test(String(value || ""));
151
+
152
+ const finiteNumber = (value, fallback) => {
153
+ const numeric = Number(value);
154
+ return Number.isFinite(numeric) ? numeric : fallback;
155
+ };
156
+
157
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);