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
|
@@ -8,9 +8,11 @@ module Vizcore
|
|
|
8
8
|
|
|
9
9
|
Point = Struct.new(:value, :unit, keyword_init: true)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# @param bpm [Numeric, nil] fixed BPM for mixed-unit timeline conversion
|
|
12
|
+
def initialize(beats_per_bar: DEFAULT_BEATS_PER_BAR, bpm: nil)
|
|
12
13
|
@beats_per_bar = positive_integer(beats_per_bar, "beats_per_bar")
|
|
13
14
|
@entries = []
|
|
15
|
+
@bpm = positive_float(bpm, "timeline bpm") unless bpm.nil?
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
# Evaluate a timeline block.
|
|
@@ -27,14 +29,16 @@ module Vizcore
|
|
|
27
29
|
#
|
|
28
30
|
# @param position [Numeric, Point] seconds by default, or a value from `seconds`, `beats`, or `bars`
|
|
29
31
|
# @param scene [Symbol, String] scene to activate at the position
|
|
32
|
+
# @param cue [Symbol, String, nil] optional cue identifier for marker metadata
|
|
30
33
|
# @return [Hash]
|
|
31
|
-
def at(position, scene:)
|
|
34
|
+
def at(position, scene:, cue: nil)
|
|
32
35
|
point = normalize_position(position)
|
|
33
36
|
entry = {
|
|
34
37
|
at: point.value,
|
|
35
38
|
unit: point.unit,
|
|
36
39
|
scene: scene.to_sym
|
|
37
40
|
}
|
|
41
|
+
entry[:cue] = cue.to_sym if cue
|
|
38
42
|
@entries << entry
|
|
39
43
|
entry
|
|
40
44
|
end
|
|
@@ -66,12 +70,13 @@ module Vizcore
|
|
|
66
70
|
|
|
67
71
|
# @return [Array<Hash>] generated scene transitions
|
|
68
72
|
def transitions
|
|
73
|
+
return [] if @entries.length < 2
|
|
74
|
+
|
|
69
75
|
@entries.each_cons(2).map do |from_entry, to_entry|
|
|
70
|
-
delta = to_entry.fetch(:at) - from_entry.fetch(:at)
|
|
71
76
|
{
|
|
72
77
|
from: from_entry.fetch(:scene),
|
|
73
78
|
to: to_entry.fetch(:scene),
|
|
74
|
-
trigger: trigger_for(
|
|
79
|
+
trigger: trigger_for(from_entry, to_entry)
|
|
75
80
|
}
|
|
76
81
|
end
|
|
77
82
|
end
|
|
@@ -84,8 +89,15 @@ module Vizcore
|
|
|
84
89
|
seconds(position)
|
|
85
90
|
end
|
|
86
91
|
|
|
87
|
-
def trigger_for(
|
|
88
|
-
|
|
92
|
+
def trigger_for(from_entry, to_entry)
|
|
93
|
+
return trigger_for_same_unit(from_entry, to_entry) unless mixed_units?(from_entry.fetch(:unit), to_entry.fetch(:unit))
|
|
94
|
+
|
|
95
|
+
trigger_for_mixed_units(from_entry, to_entry)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def trigger_for_same_unit(from_entry, to_entry)
|
|
99
|
+
delta = to_entry.fetch(:at) - from_entry.fetch(:at)
|
|
100
|
+
case from_entry.fetch(:unit)
|
|
89
101
|
when :seconds
|
|
90
102
|
proc { seconds >= delta }
|
|
91
103
|
when :beats
|
|
@@ -95,12 +107,56 @@ module Vizcore
|
|
|
95
107
|
end
|
|
96
108
|
end
|
|
97
109
|
|
|
110
|
+
def trigger_for_mixed_units(from_entry, to_entry)
|
|
111
|
+
fixed_bpm = @bpm
|
|
112
|
+
|
|
113
|
+
if fixed_bpm
|
|
114
|
+
from_position = marker_position_seconds(from_entry, fixed_bpm: fixed_bpm)
|
|
115
|
+
to_position = marker_position_seconds(to_entry, fixed_bpm: fixed_bpm)
|
|
116
|
+
if from_position && to_position
|
|
117
|
+
return proc { seconds >= (to_position - from_position) }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
convert_position = lambda do |entry, bpm|
|
|
122
|
+
value = entry.fetch(:at)
|
|
123
|
+
case entry.fetch(:unit)
|
|
124
|
+
when :seconds
|
|
125
|
+
value
|
|
126
|
+
when :beats
|
|
127
|
+
return nil unless bpm.to_f.positive?
|
|
128
|
+
|
|
129
|
+
value * 60.0 / Float(bpm)
|
|
130
|
+
else
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
proc do
|
|
136
|
+
used_bpm = fixed_bpm || bpm
|
|
137
|
+
from_position = convert_position.call(from_entry, used_bpm)
|
|
138
|
+
to_position = convert_position.call(to_entry, used_bpm)
|
|
139
|
+
return false unless from_position && to_position
|
|
140
|
+
|
|
141
|
+
seconds >= (to_position - from_position)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
98
145
|
def validate_entries!
|
|
99
146
|
return if @entries.length < 2
|
|
100
147
|
|
|
101
|
-
unit = @entries.first.fetch(:unit)
|
|
102
148
|
@entries.each_cons(2) do |from_entry, to_entry|
|
|
103
|
-
|
|
149
|
+
if mixed_units?(from_entry.fetch(:unit), to_entry.fetch(:unit))
|
|
150
|
+
if @bpm
|
|
151
|
+
from_position = marker_position_seconds(from_entry, fixed_bpm: @bpm)
|
|
152
|
+
to_position = marker_position_seconds(to_entry, fixed_bpm: @bpm)
|
|
153
|
+
raise ArgumentError, "timeline entries must increase when converted to seconds" if to_position.nil? || from_position.nil? || to_position <= from_position
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
next
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
raise ArgumentError, "timeline entries must use the same unit" unless to_entry.fetch(:unit) == from_entry.fetch(:unit)
|
|
104
160
|
|
|
105
161
|
from_position = from_entry.fetch(:at)
|
|
106
162
|
to_position = to_entry.fetch(:at)
|
|
@@ -108,6 +164,24 @@ module Vizcore
|
|
|
108
164
|
end
|
|
109
165
|
end
|
|
110
166
|
|
|
167
|
+
def mixed_units?(left_unit, right_unit)
|
|
168
|
+
left_unit != right_unit
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def marker_position_seconds(entry, fixed_bpm:)
|
|
172
|
+
value = entry.fetch(:at)
|
|
173
|
+
case entry.fetch(:unit)
|
|
174
|
+
when :seconds
|
|
175
|
+
value
|
|
176
|
+
when :beats
|
|
177
|
+
return nil unless fixed_bpm.to_f.positive?
|
|
178
|
+
|
|
179
|
+
value * 60.0 / Float(fixed_bpm)
|
|
180
|
+
else
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
111
185
|
def non_negative_float(value, name)
|
|
112
186
|
numeric = parse_float(value, name)
|
|
113
187
|
raise ArgumentError, "#{name} must be non-negative" if numeric.negative?
|
|
@@ -133,6 +207,15 @@ module Vizcore
|
|
|
133
207
|
rescue ArgumentError, TypeError
|
|
134
208
|
raise ArgumentError, "#{name} must be an integer"
|
|
135
209
|
end
|
|
210
|
+
|
|
211
|
+
def positive_float(value, name)
|
|
212
|
+
numeric = Float(value)
|
|
213
|
+
raise ArgumentError, "#{name} must be positive" unless numeric.positive?
|
|
214
|
+
|
|
215
|
+
numeric
|
|
216
|
+
rescue ArgumentError, TypeError
|
|
217
|
+
raise ArgumentError, "#{name} must be numeric"
|
|
218
|
+
end
|
|
136
219
|
end
|
|
137
220
|
end
|
|
138
221
|
end
|
|
@@ -8,7 +8,9 @@ module Vizcore
|
|
|
8
8
|
|
|
9
9
|
# @param scenes [Array<Hash>]
|
|
10
10
|
# @param transitions [Array<Hash>]
|
|
11
|
-
|
|
11
|
+
# @param error_reporter [#call, nil]
|
|
12
|
+
def initialize(scenes:, transitions:, error_reporter: nil)
|
|
13
|
+
@error_reporter = error_reporter || ->(_message) {}
|
|
12
14
|
update(scenes: scenes, transitions: transitions)
|
|
13
15
|
end
|
|
14
16
|
|
|
@@ -23,11 +25,12 @@ module Vizcore
|
|
|
23
25
|
# @param scene_name [String, Symbol]
|
|
24
26
|
# @param audio [Hash]
|
|
25
27
|
# @param frame_count [Integer]
|
|
28
|
+
# @param elapsed_seconds [Numeric, nil]
|
|
26
29
|
# @return [Hash, nil] transition payload when condition matches
|
|
27
|
-
def next_transition(scene_name:, audio:, frame_count: 0)
|
|
30
|
+
def next_transition(scene_name:, audio:, frame_count: 0, elapsed_seconds: nil)
|
|
28
31
|
current = scene_name.to_sym
|
|
29
32
|
transition = @transitions.find do |entry|
|
|
30
|
-
entry[:from] == current && trigger_match?(entry
|
|
33
|
+
entry[:from] == current && trigger_match?(entry, audio, frame_count, elapsed_seconds)
|
|
31
34
|
end
|
|
32
35
|
return nil unless transition
|
|
33
36
|
|
|
@@ -73,14 +76,24 @@ module Vizcore
|
|
|
73
76
|
end
|
|
74
77
|
end
|
|
75
78
|
|
|
76
|
-
def trigger_match?(
|
|
79
|
+
def trigger_match?(transition, audio, frame_count, elapsed_seconds)
|
|
80
|
+
trigger = transition[:trigger]
|
|
77
81
|
return false unless trigger.respond_to?(:call)
|
|
78
82
|
|
|
79
|
-
TriggerContext.new(audio, frame_count: frame_count).instance_exec(&trigger)
|
|
80
|
-
rescue StandardError
|
|
83
|
+
TriggerContext.new(audio, frame_count: frame_count, elapsed_seconds: elapsed_seconds).instance_exec(&trigger)
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
report_trigger_error(transition, e)
|
|
81
86
|
false
|
|
82
87
|
end
|
|
83
88
|
|
|
89
|
+
def report_trigger_error(transition, error)
|
|
90
|
+
@error_reporter.call(
|
|
91
|
+
"transition trigger failed: #{transition[:from]} -> #{transition[:to]} (#{error.class}: #{error.message})"
|
|
92
|
+
)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
84
97
|
def symbolize_hash(value)
|
|
85
98
|
Hash(value).each_with_object({}) do |(key, entry), output|
|
|
86
99
|
output[key.to_sym] = entry
|
|
@@ -90,16 +103,7 @@ module Vizcore
|
|
|
90
103
|
end
|
|
91
104
|
|
|
92
105
|
def deep_dup(value)
|
|
93
|
-
|
|
94
|
-
when Hash
|
|
95
|
-
value.each_with_object({}) do |(key, entry), output|
|
|
96
|
-
output[key] = deep_dup(entry)
|
|
97
|
-
end
|
|
98
|
-
when Array
|
|
99
|
-
value.map { |entry| deep_dup(entry) }
|
|
100
|
-
else
|
|
101
|
-
value
|
|
102
|
-
end
|
|
106
|
+
Vizcore::DeepCopy.copy(value)
|
|
103
107
|
end
|
|
104
108
|
|
|
105
109
|
# Runtime DSL context exposed to transition trigger blocks.
|
|
@@ -107,14 +111,18 @@ module Vizcore
|
|
|
107
111
|
class TriggerContext
|
|
108
112
|
# @param audio [Hash]
|
|
109
113
|
# @param frame_count [Integer]
|
|
110
|
-
|
|
114
|
+
# @param elapsed_seconds [Numeric, nil]
|
|
115
|
+
def initialize(audio, frame_count:, elapsed_seconds: nil)
|
|
111
116
|
@audio = symbolize_hash(audio)
|
|
112
117
|
@bands = symbolize_hash(@audio[:bands])
|
|
118
|
+
@band_peaks = symbolize_hash(@audio[:band_peaks])
|
|
113
119
|
@onsets = symbolize_hash(@audio[:onsets])
|
|
114
120
|
@drums = symbolize_hash(@audio[:drums])
|
|
115
121
|
@frame_count = Integer(frame_count)
|
|
122
|
+
@elapsed_seconds = normalize_elapsed_seconds(elapsed_seconds)
|
|
116
123
|
rescue StandardError
|
|
117
124
|
@frame_count = 0
|
|
125
|
+
@elapsed_seconds = nil
|
|
118
126
|
end
|
|
119
127
|
|
|
120
128
|
# @return [Float]
|
|
@@ -122,42 +130,83 @@ module Vizcore
|
|
|
122
130
|
@audio[:amplitude].to_f
|
|
123
131
|
end
|
|
124
132
|
|
|
133
|
+
# @return [Float]
|
|
134
|
+
def peak
|
|
135
|
+
@audio[:peak].to_f
|
|
136
|
+
end
|
|
137
|
+
|
|
125
138
|
# @param name [Symbol, String]
|
|
126
139
|
# @return [Float]
|
|
127
140
|
def frequency_band(name)
|
|
128
141
|
@bands[name.to_sym].to_f
|
|
129
142
|
end
|
|
130
143
|
|
|
144
|
+
# @param name [Symbol, String]
|
|
145
|
+
# @return [Float]
|
|
146
|
+
def frequency_band_peak(name)
|
|
147
|
+
@band_peaks[name.to_sym].to_f
|
|
148
|
+
end
|
|
149
|
+
|
|
131
150
|
# @return [Float]
|
|
132
151
|
def sub
|
|
133
152
|
frequency_band(:sub)
|
|
134
153
|
end
|
|
135
154
|
|
|
155
|
+
# @return [Float]
|
|
156
|
+
def sub_peak
|
|
157
|
+
frequency_band_peak(:sub)
|
|
158
|
+
end
|
|
159
|
+
|
|
136
160
|
# @return [Float]
|
|
137
161
|
def low
|
|
138
162
|
frequency_band(:low)
|
|
139
163
|
end
|
|
140
164
|
|
|
165
|
+
# @return [Float]
|
|
166
|
+
def low_peak
|
|
167
|
+
frequency_band_peak(:low)
|
|
168
|
+
end
|
|
169
|
+
|
|
141
170
|
# @return [Float]
|
|
142
171
|
def bass
|
|
143
172
|
frequency_band(:low)
|
|
144
173
|
end
|
|
145
174
|
|
|
175
|
+
# @return [Float]
|
|
176
|
+
def bass_peak
|
|
177
|
+
frequency_band_peak(:low)
|
|
178
|
+
end
|
|
179
|
+
|
|
146
180
|
# @return [Float]
|
|
147
181
|
def mid
|
|
148
182
|
frequency_band(:mid)
|
|
149
183
|
end
|
|
150
184
|
|
|
185
|
+
# @return [Float]
|
|
186
|
+
def mid_peak
|
|
187
|
+
frequency_band_peak(:mid)
|
|
188
|
+
end
|
|
189
|
+
|
|
151
190
|
# @return [Float]
|
|
152
191
|
def high
|
|
153
192
|
frequency_band(:high)
|
|
154
193
|
end
|
|
155
194
|
|
|
195
|
+
# @return [Float]
|
|
196
|
+
def high_peak
|
|
197
|
+
frequency_band_peak(:high)
|
|
198
|
+
end
|
|
199
|
+
|
|
156
200
|
# @return [Float]
|
|
157
201
|
def treble
|
|
158
202
|
frequency_band(:high)
|
|
159
203
|
end
|
|
160
204
|
|
|
205
|
+
# @return [Float]
|
|
206
|
+
def treble_peak
|
|
207
|
+
frequency_band_peak(:high)
|
|
208
|
+
end
|
|
209
|
+
|
|
161
210
|
# @return [Array<Float>]
|
|
162
211
|
def fft_spectrum
|
|
163
212
|
Array(@audio[:fft])
|
|
@@ -213,23 +262,113 @@ module Vizcore
|
|
|
213
262
|
0
|
|
214
263
|
end
|
|
215
264
|
|
|
265
|
+
# @return [Float]
|
|
266
|
+
def beat_phase
|
|
267
|
+
@audio[:beat_phase].to_f
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# @return [Boolean]
|
|
271
|
+
def beat_2
|
|
272
|
+
!!@audio[:beat_2]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# @return [Boolean]
|
|
276
|
+
def beat_4
|
|
277
|
+
!!@audio[:beat_4]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# @return [Boolean]
|
|
281
|
+
def beat_8
|
|
282
|
+
!!@audio[:beat_8]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# @return [Boolean]
|
|
286
|
+
def beat_triplet
|
|
287
|
+
!!@audio[:beat_triplet]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# @return [Boolean]
|
|
291
|
+
def triplet
|
|
292
|
+
beat_triplet
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# @return [Float]
|
|
296
|
+
def bar_phase
|
|
297
|
+
@audio[:bar_phase].to_f
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# @return [Integer]
|
|
301
|
+
def bar_count
|
|
302
|
+
Integer(@audio[:bar_count] || 0)
|
|
303
|
+
rescue StandardError
|
|
304
|
+
0
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# @return [Integer]
|
|
308
|
+
def phrase_count
|
|
309
|
+
Integer(@audio[:phrase_count] || 0)
|
|
310
|
+
rescue StandardError
|
|
311
|
+
0
|
|
312
|
+
end
|
|
313
|
+
|
|
216
314
|
# @return [Float]
|
|
217
315
|
def bpm
|
|
218
316
|
@audio[:bpm].to_f
|
|
219
317
|
end
|
|
220
318
|
|
|
319
|
+
# @return [Float]
|
|
320
|
+
def bpm_confidence
|
|
321
|
+
@audio[:bpm_confidence].to_f
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# @return [Float]
|
|
325
|
+
def spectral_centroid
|
|
326
|
+
@audio[:spectral_centroid].to_f
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# @return [Float]
|
|
330
|
+
def spectral_rolloff
|
|
331
|
+
@audio[:spectral_rolloff].to_f
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# @return [Float]
|
|
335
|
+
def spectral_flatness
|
|
336
|
+
@audio[:spectral_flatness].to_f
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# @return [Float]
|
|
340
|
+
def spectral_flux
|
|
341
|
+
@audio[:spectral_flux].to_f
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# @return [Float]
|
|
345
|
+
def zero_crossing_rate
|
|
346
|
+
@audio[:zero_crossing_rate].to_f
|
|
347
|
+
end
|
|
348
|
+
|
|
221
349
|
# @return [Integer]
|
|
222
350
|
def frame_count
|
|
223
351
|
@frame_count
|
|
224
352
|
end
|
|
225
353
|
|
|
226
|
-
# @return [Float] scene-local elapsed seconds
|
|
354
|
+
# @return [Float] scene-local elapsed seconds
|
|
227
355
|
def seconds
|
|
356
|
+
return @elapsed_seconds if @elapsed_seconds
|
|
357
|
+
|
|
228
358
|
@frame_count / DEFAULT_FRAME_RATE
|
|
229
359
|
end
|
|
230
360
|
|
|
231
361
|
private
|
|
232
362
|
|
|
363
|
+
def normalize_elapsed_seconds(value)
|
|
364
|
+
return nil if value.nil?
|
|
365
|
+
|
|
366
|
+
numeric = Float(value)
|
|
367
|
+
numeric.finite? ? numeric : nil
|
|
368
|
+
rescue StandardError
|
|
369
|
+
nil
|
|
370
|
+
end
|
|
371
|
+
|
|
233
372
|
def symbolize_hash(value)
|
|
234
373
|
Hash(value).each_with_object({}) do |(key, entry), output|
|
|
235
374
|
output[key.to_sym] = entry
|
data/lib/vizcore/dsl.rb
CHANGED
|
@@ -6,11 +6,13 @@ module Vizcore
|
|
|
6
6
|
end
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
require_relative "deep_copy"
|
|
9
10
|
require_relative "dsl/file_watcher"
|
|
10
11
|
require_relative "dsl/mapping_resolver"
|
|
11
12
|
require_relative "dsl/mapping_transform_builder"
|
|
12
13
|
require_relative "dsl/midi_map_executor"
|
|
13
14
|
require_relative "dsl/reaction_builder"
|
|
15
|
+
require_relative "dsl/mapping_preset_builder"
|
|
14
16
|
require_relative "dsl/style_builder"
|
|
15
17
|
require_relative "dsl/layer_builder"
|
|
16
18
|
require_relative "dsl/scene_builder"
|
|
@@ -19,6 +19,7 @@ module Vizcore
|
|
|
19
19
|
opacity: "Float",
|
|
20
20
|
blend: "Symbol",
|
|
21
21
|
effect: "Symbol",
|
|
22
|
+
post_effects: "Array<Symbol>",
|
|
22
23
|
effect_intensity: "Float",
|
|
23
24
|
vj_effect: "Symbol",
|
|
24
25
|
palette: "Array<String>",
|
|
@@ -151,10 +152,12 @@ module Vizcore
|
|
|
151
152
|
aliases: %i[shapes shape_layer],
|
|
152
153
|
params: COMMON_PARAMS.merge(
|
|
153
154
|
shapes: "Array<Hash>",
|
|
155
|
+
shape_schema_version: "Integer",
|
|
156
|
+
units: "Symbol",
|
|
154
157
|
color_shift: "Float"
|
|
155
158
|
),
|
|
156
|
-
mappable_params: %i[color_shift opacity],
|
|
157
|
-
description: "Declarative 2D circle and
|
|
159
|
+
mappable_params: %i[color_shift opacity shapes],
|
|
160
|
+
description: "Declarative and Ruby-generated 2D circle, line, rect, polygon, polyline, path, and star primitives rendered by the browser."
|
|
158
161
|
),
|
|
159
162
|
Capability.new(
|
|
160
163
|
type: :mesh,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "rack"
|
|
5
|
+
|
|
6
|
+
module Vizcore
|
|
7
|
+
# Validates browser-side plugin assets before they are served by the runtime.
|
|
8
|
+
class PluginAssetPolicy
|
|
9
|
+
ALLOWED_EXTENSIONS = %w[.js .mjs].freeze
|
|
10
|
+
ALLOWED_MIME_TYPES = %w[text/javascript application/javascript].freeze
|
|
11
|
+
|
|
12
|
+
# @param path [String, Pathname]
|
|
13
|
+
# @param root [String, Pathname, nil] optional sandbox root
|
|
14
|
+
# @return [Pathname]
|
|
15
|
+
def self.validate!(path, root: nil)
|
|
16
|
+
asset_path = Pathname.new(path).expand_path
|
|
17
|
+
root_path = root ? Pathname.new(root).expand_path : nil
|
|
18
|
+
|
|
19
|
+
if root_path && !inside_root?(asset_path, root_path)
|
|
20
|
+
raise ArgumentError, "Plugin asset must stay inside #{root_path}: #{asset_path}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
unless ALLOWED_EXTENSIONS.include?(asset_path.extname.downcase)
|
|
24
|
+
raise ArgumentError, "Unsupported plugin asset extension: #{asset_path.extname}. Use one of: #{ALLOWED_EXTENSIONS.join(', ')}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
extname = asset_path.extname.downcase
|
|
28
|
+
mime_type = Rack::Mime.mime_type(extname, "application/octet-stream")
|
|
29
|
+
mime_type = case extname
|
|
30
|
+
when ".mjs"
|
|
31
|
+
mime_type == "application/octet-stream" ? "application/javascript" : mime_type
|
|
32
|
+
when ".js"
|
|
33
|
+
mime_type == "application/octet-stream" ? "text/javascript" : mime_type
|
|
34
|
+
else
|
|
35
|
+
mime_type
|
|
36
|
+
end
|
|
37
|
+
unless ALLOWED_MIME_TYPES.include?(mime_type)
|
|
38
|
+
raise ArgumentError, "Unsupported plugin asset MIME type: #{mime_type}. Use one of: #{ALLOWED_MIME_TYPES.join(", ")}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
asset_path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param path [Pathname]
|
|
45
|
+
# @param root [Pathname]
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def self.inside_root?(path, root)
|
|
48
|
+
relative = path.relative_path_from(root)
|
|
49
|
+
!relative.each_filename.first&.start_with?("..")
|
|
50
|
+
rescue ArgumentError
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
private_class_method :inside_root?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
4
|
require "yaml"
|
|
5
|
+
require_relative "plugin_asset_policy"
|
|
5
6
|
|
|
6
7
|
module Vizcore
|
|
7
8
|
# Reads a project-level manifest such as vizcore.yml.
|
|
@@ -36,6 +37,8 @@ module Vizcore
|
|
|
36
37
|
feature_file: expand_path(value_at(data, "feature_file") || value_at(data, "features")),
|
|
37
38
|
control_preset: expand_path(value_at(data, "control_preset") || value_at(data, "controlPreset")),
|
|
38
39
|
osc_port: value_at(data, "osc_port") || value_at(data, "sync", "osc_port") || value_at(data, "sync", "osc", "port"),
|
|
40
|
+
scene_switch_effect: value_at(data, "scene_switch_effect"),
|
|
41
|
+
scene_switch_effect_duration: value_at(data, "scene_switch_effect_duration"),
|
|
39
42
|
plugin_assets: plugin_assets(profile: profile)
|
|
40
43
|
}.compact
|
|
41
44
|
end
|
|
@@ -49,7 +52,8 @@ module Vizcore
|
|
|
49
52
|
def plugin_assets(profile: nil)
|
|
50
53
|
entries = plugin_entries(profile: profile)
|
|
51
54
|
assets = base_values("plugin_assets", "frontend_plugins") + profile_values(profile, "plugin_assets", "frontend_plugins")
|
|
52
|
-
|
|
55
|
+
manifest_assets = assets.filter_map { |entry| validate_plugin_asset_path(expand_path(entry)) }
|
|
56
|
+
(entries.filter_map { |entry| plugin_asset_path(entry) } + manifest_assets).uniq
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
# @return [Array<String>] configured profile names
|
|
@@ -124,7 +128,7 @@ module Vizcore
|
|
|
124
128
|
def plugin_asset_path(entry)
|
|
125
129
|
return nil unless entry.is_a?(Hash)
|
|
126
130
|
|
|
127
|
-
expand_path(entry["asset"] || entry[:asset] || entry["frontend"] || entry[:frontend])
|
|
131
|
+
validate_plugin_asset_path(expand_path(entry["asset"] || entry[:asset] || entry["frontend"] || entry[:frontend]))
|
|
128
132
|
end
|
|
129
133
|
|
|
130
134
|
def expand_path(value)
|
|
@@ -135,6 +139,12 @@ module Vizcore
|
|
|
135
139
|
path_value.absolute? ? path_value : @root.join(path_value).expand_path
|
|
136
140
|
end
|
|
137
141
|
|
|
142
|
+
def validate_plugin_asset_path(path)
|
|
143
|
+
return nil unless path
|
|
144
|
+
|
|
145
|
+
Vizcore::PluginAssetPolicy.validate!(path, root: @root)
|
|
146
|
+
end
|
|
147
|
+
|
|
138
148
|
def deep_merge(base, overlay)
|
|
139
149
|
base.merge(overlay) do |_key, left, right|
|
|
140
150
|
left.is_a?(Hash) && right.is_a?(Hash) ? deep_merge(left, right) : right
|