vizcore 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/frontend/index.html +24 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +447 -57
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +391 -10
- data/frontend/src/renderer/layer-manager.js +472 -71
- 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/visuals/image-renderer.js +19 -0
- data/frontend/src/visuals/particle-system.js +10 -0
- data/frontend/src/visuals/shape-renderer.js +13 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -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 +64 -8
- 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 +487 -39
- 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 +278 -15
- 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 +404 -22
- 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 +1 -0
- 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 +179 -14
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +469 -23
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +676 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +39 -16
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +33 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +55 -4
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7944c441173090fb3e3ed70588cc7a6705697c140f516bc9502c5208c76577d3
|
|
4
|
+
data.tar.gz: 34896b6dedbb509e42d36a36a22d19f775e1f1a56031890bcf1d295744e331bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3f86f14c01cfc947279eab536433e6f95fd2ee1abdaa0b1e55fc617af28c555f2241bc929113679fc247dd847099c08bc554a7126dacda16b8c689773f9f59d4
|
|
7
|
+
data.tar.gz: 2ec0d8c023f3657250e5752475df36c7c50dac117153df03618c9bdb21c19f7f8bcdbd5028c365be96e9fc633a8b62a718a32d04fc5edca2f08f7d00cf74e93a
|
data/frontend/index.html
CHANGED
|
@@ -459,6 +459,7 @@
|
|
|
459
459
|
<p id="ws-status">WebSocket: connecting...</p>
|
|
460
460
|
<p id="scene-status">Scene: unknown</p>
|
|
461
461
|
<p id="transition-status">Transition: none</p>
|
|
462
|
+
<p id="runtime-error-status">Runtime: ok</p>
|
|
462
463
|
<p id="frame-status">Amplitude: 0.0000</p>
|
|
463
464
|
<p id="bpm-status" class="is-accent">BPM: --</p>
|
|
464
465
|
<p id="beat-status">Beat: off | Count: 0</p>
|
|
@@ -467,7 +468,7 @@
|
|
|
467
468
|
<button id="freeze-toggle" type="button" aria-pressed="false">Freeze</button>
|
|
468
469
|
<p id="live-control-status" class="live-controls__status">Live: output</p>
|
|
469
470
|
</div>
|
|
470
|
-
<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>
|
|
471
472
|
<div class="audio-inspector" aria-label="Audio inspector">
|
|
472
473
|
<div class="audio-inspector__header">
|
|
473
474
|
<span>Audio</span>
|
|
@@ -501,6 +502,7 @@
|
|
|
501
502
|
<div id="fft-preview" class="fft-preview" aria-label="FFT preview"></div>
|
|
502
503
|
</div>
|
|
503
504
|
<p id="audio-source-status">Audio Source: unknown</p>
|
|
505
|
+
<p id="audio-health-status">Audio Health: unknown</p>
|
|
504
506
|
<p id="audio-track-status">Track: none</p>
|
|
505
507
|
<p id="audio-playback-status">Playback: unavailable</p>
|
|
506
508
|
<div class="reactivity-controls" aria-label="Visual reactivity controls">
|
|
@@ -549,7 +551,12 @@
|
|
|
549
551
|
window.__vizcoreMainStarted = false;
|
|
550
552
|
(function () {
|
|
551
553
|
var protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
552
|
-
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);
|
|
553
560
|
var fallbackTimer = window.setTimeout(function () {
|
|
554
561
|
if (window.__vizcoreMainStarted) return;
|
|
555
562
|
|
|
@@ -559,6 +566,7 @@
|
|
|
559
566
|
var bpmStatus = document.getElementById("bpm-status");
|
|
560
567
|
var beatStatus = document.getElementById("beat-status");
|
|
561
568
|
var audioSourceStatus = document.getElementById("audio-source-status");
|
|
569
|
+
var audioHealthStatus = document.getElementById("audio-health-status");
|
|
562
570
|
var peakStatus = document.getElementById("inspector-peak");
|
|
563
571
|
var ampValue = document.getElementById("inspector-amplitude-value");
|
|
564
572
|
var ampFill = document.getElementById("inspector-amplitude-fill");
|
|
@@ -582,6 +590,20 @@
|
|
|
582
590
|
if (runtime && audioSourceStatus) {
|
|
583
591
|
audioSourceStatus.textContent = "Audio Source: " + runtime.audio_source;
|
|
584
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
|
+
}
|
|
585
607
|
})
|
|
586
608
|
.catch(function () {});
|
|
587
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
|
};
|
|
@@ -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
|
+
};
|