vizcore 0.1.0 → 1.0.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 +544 -9
- data/docs/.nojekyll +0 -0
- data/docs/assets/site.css +744 -0
- data/docs/assets/vizcore-demo.gif +0 -0
- data/docs/assets/vizcore-poster.png +0 -0
- data/docs/assets/vj-tunnel.js +159 -0
- data/docs/index.html +224 -0
- data/examples/README.md +59 -0
- data/examples/assets/README.md +19 -0
- data/examples/audio_inspector.rb +34 -0
- data/examples/club_intro_drop.rb +78 -0
- data/examples/kansai_rubykaigi_visual.rb +70 -0
- data/examples/live_coding_minimal.rb +22 -0
- data/examples/midi_controller_show.rb +78 -0
- data/examples/midi_scene_switch.rb +3 -1
- data/examples/parser_visualizer.rb +48 -0
- data/examples/readme_demo.rb +17 -0
- data/examples/rhythm_geometry.rb +34 -0
- data/examples/ruby_crystal_show.rb +35 -0
- data/examples/shader_playground.rb +18 -0
- data/examples/unyo_liquid.rb +59 -0
- data/examples/vj_ambient_chill_room.rb +124 -0
- data/examples/vj_dnb_jungle.rb +170 -0
- data/examples/vj_festival_mainstage.rb +245 -0
- data/examples/vj_festival_mainstage.yml +17 -0
- data/examples/vj_glitch_industrial.rb +164 -0
- data/examples/vj_hiphop_cipher.rb +167 -0
- data/examples/vj_jpop_idol_live.rb +210 -0
- data/examples/vj_synthwave_retro.rb +173 -0
- data/examples/vj_techno_warehouse.rb +195 -0
- data/frontend/index.html +468 -2
- data/frontend/src/audio-inspector.js +40 -0
- data/frontend/src/live-controls.js +131 -0
- data/frontend/src/main.js +792 -16
- data/frontend/src/midi-learn.js +194 -0
- data/frontend/src/performance-monitor.js +183 -0
- data/frontend/src/plugin-runtime.js +130 -0
- data/frontend/src/projector-mode.js +56 -0
- data/frontend/src/renderer/engine.js +148 -3
- data/frontend/src/renderer/layer-manager.js +428 -30
- data/frontend/src/renderer/shader-manager.js +26 -0
- data/frontend/src/runtime-control-preset.js +11 -0
- data/frontend/src/shader-error-overlay.js +29 -0
- data/frontend/src/shader-param-controls.js +93 -0
- data/frontend/src/shaders/builtins.js +380 -2
- data/frontend/src/shaders/post-effects.js +52 -0
- data/frontend/src/visual-regression.js +67 -0
- data/frontend/src/visual-settings-preset.js +103 -0
- data/frontend/src/visuals/geometry.js +268 -0
- data/frontend/src/visuals/image-renderer.js +291 -0
- data/frontend/src/visuals/particle-system.js +56 -10
- data/frontend/src/visuals/spectrogram-renderer.js +226 -0
- data/frontend/src/visuals/text-renderer.js +112 -11
- data/frontend/src/websocket-client.js +12 -1
- data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
- data/lib/vizcore/analysis/beat_detector.rb +4 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
- data/lib/vizcore/analysis/feature_recorder.rb +159 -0
- data/lib/vizcore/analysis/feature_replay.rb +84 -0
- data/lib/vizcore/analysis/pipeline.rb +235 -11
- data/lib/vizcore/analysis/tap_tempo.rb +74 -0
- data/lib/vizcore/analysis.rb +4 -0
- data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
- data/lib/vizcore/audio/fixture_input.rb +65 -0
- data/lib/vizcore/audio/input_manager.rb +4 -2
- data/lib/vizcore/audio/mic_input.rb +24 -8
- data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/doctor.rb +159 -0
- data/lib/vizcore/cli/dsl_reference.rb +99 -0
- data/lib/vizcore/cli/layer_docs.rb +46 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
- data/lib/vizcore/cli/scene_inspector.rb +136 -0
- data/lib/vizcore/cli/scene_validator.rb +245 -0
- data/lib/vizcore/cli/shader_template.rb +68 -0
- data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
- data/lib/vizcore/cli.rb +689 -18
- data/lib/vizcore/config.rb +103 -2
- data/lib/vizcore/control_preset.rb +68 -0
- data/lib/vizcore/dsl/engine.rb +277 -5
- data/lib/vizcore/dsl/layer_builder.rb +491 -22
- data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +132 -3
- data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
- data/lib/vizcore/dsl/reaction_builder.rb +44 -0
- data/lib/vizcore/dsl/scene_builder.rb +61 -5
- data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
- data/lib/vizcore/dsl/style_builder.rb +68 -0
- data/lib/vizcore/dsl/timeline_builder.rb +138 -0
- data/lib/vizcore/dsl/transition_controller.rb +77 -0
- data/lib/vizcore/dsl.rb +5 -1
- data/lib/vizcore/layer_catalog.rb +273 -0
- data/lib/vizcore/project_manifest.rb +152 -0
- data/lib/vizcore/renderer/png_writer.rb +57 -0
- data/lib/vizcore/renderer/render_sequence.rb +153 -0
- data/lib/vizcore/renderer/scene_frame_source.rb +119 -0
- data/lib/vizcore/renderer/scene_serializer.rb +36 -3
- data/lib/vizcore/renderer/snapshot.rb +38 -0
- data/lib/vizcore/renderer/snapshot_renderer.rb +446 -0
- data/lib/vizcore/renderer.rb +5 -0
- data/lib/vizcore/server/frame_broadcaster.rb +91 -5
- data/lib/vizcore/server/gallery_app.rb +155 -0
- data/lib/vizcore/server/gallery_page.rb +100 -0
- data/lib/vizcore/server/gallery_runner.rb +48 -0
- data/lib/vizcore/server/rack_app.rb +203 -4
- data/lib/vizcore/server/runner.rb +370 -22
- data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
- data/lib/vizcore/server/websocket_handler.rb +60 -10
- data/lib/vizcore/server.rb +4 -0
- data/lib/vizcore/sync/osc_message.rb +103 -0
- data/lib/vizcore/sync/osc_receiver.rb +68 -0
- data/lib/vizcore/sync.rb +4 -0
- data/lib/vizcore/templates/midi_control_scene.rb +3 -1
- data/lib/vizcore/templates/plugin_layer.rb +20 -0
- data/lib/vizcore/templates/plugin_readme.md +23 -0
- data/lib/vizcore/templates/plugin_renderer.js +43 -0
- data/lib/vizcore/templates/plugin_scene.rb +14 -0
- data/lib/vizcore/templates/project_readme.md +7 -23
- data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +27 -0
- data/scripts/browser_capture.mjs +75 -0
- data/sig/vizcore.rbs +362 -0
- metadata +83 -3
- data/docs/GETTING_STARTED.md +0 -105
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "mapping_transform_builder"
|
|
4
|
+
require_relative "reaction_builder"
|
|
5
|
+
|
|
3
6
|
module Vizcore
|
|
4
7
|
module DSL
|
|
5
8
|
# Builder for one render layer in a scene.
|
|
6
9
|
class LayerBuilder
|
|
10
|
+
NO_ARGUMENT = Object.new.freeze
|
|
11
|
+
MAPPING_SOURCE_KINDS = %i[
|
|
12
|
+
amplitude frequency_band fft_spectrum onset kick snare hihat beat beat_confidence beat_pulse beat_count bpm
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
7
15
|
# @param name [Symbol, String] layer identifier
|
|
8
|
-
|
|
16
|
+
# @param styles [Hash] reusable layer parameter styles
|
|
17
|
+
# @param defaults [Hash] default params applied before layer-specific values
|
|
18
|
+
def initialize(name:, styles: {}, defaults: {})
|
|
9
19
|
@name = name.to_sym
|
|
20
|
+
@styles = styles
|
|
10
21
|
@type = nil
|
|
11
22
|
@shader = nil
|
|
12
23
|
@glsl = nil
|
|
13
|
-
@params =
|
|
24
|
+
@params = deep_dup(defaults)
|
|
25
|
+
@param_schema = {}
|
|
14
26
|
@mappings = []
|
|
15
27
|
end
|
|
16
28
|
|
|
@@ -29,10 +41,16 @@ module Vizcore
|
|
|
29
41
|
@type = value.to_sym
|
|
30
42
|
end
|
|
31
43
|
|
|
32
|
-
# @param value [Symbol, String] built-in shader key
|
|
44
|
+
# @param value [Symbol, String] built-in shader key or custom GLSL path
|
|
45
|
+
# @param reload [Boolean, nil] accepted for custom shader path compatibility
|
|
33
46
|
# @return [Symbol]
|
|
34
|
-
def shader(value)
|
|
35
|
-
|
|
47
|
+
def shader(value, reload: nil)
|
|
48
|
+
if shader_path?(value)
|
|
49
|
+
@glsl = value.to_s
|
|
50
|
+
@params[:shader_reload] = !!reload unless reload.nil?
|
|
51
|
+
else
|
|
52
|
+
@shader = value.to_sym
|
|
53
|
+
end
|
|
36
54
|
@type ||= :shader
|
|
37
55
|
end
|
|
38
56
|
|
|
@@ -43,6 +61,49 @@ module Vizcore
|
|
|
43
61
|
@type ||= :shader
|
|
44
62
|
end
|
|
45
63
|
|
|
64
|
+
# @param path [String, Pathname] asset file path used by media-like layers
|
|
65
|
+
# @return [String]
|
|
66
|
+
def file(path)
|
|
67
|
+
@params[:file] = path.to_s
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Declare a 2D circle/ring primitive for a shape layer.
|
|
71
|
+
#
|
|
72
|
+
# @param options [Hash] shape params such as `count`, `radius`, `x`, and `y`
|
|
73
|
+
# @yield optional block evaluated in the shape context
|
|
74
|
+
# @return [Hash]
|
|
75
|
+
def circle(**options, &block)
|
|
76
|
+
build_shape(:circle, options, &block)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Declare a 2D line primitive for a shape layer.
|
|
80
|
+
#
|
|
81
|
+
# @param options [Hash] shape params such as `x1`, `y1`, `x2`, and `y2`
|
|
82
|
+
# @yield optional block evaluated in the shape context
|
|
83
|
+
# @return [Hash]
|
|
84
|
+
def line(**options, &block)
|
|
85
|
+
build_shape(:line, options, &block)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Group shape primitives in a block for readability.
|
|
89
|
+
#
|
|
90
|
+
# @yield shape declarations
|
|
91
|
+
# @return [Array<Hash>]
|
|
92
|
+
def draw(&block)
|
|
93
|
+
@type ||= :shape
|
|
94
|
+
instance_eval(&block) if block
|
|
95
|
+
@params[:shapes] || []
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# @param value [Symbol, String] input source for media-like layers
|
|
99
|
+
# @return [Symbol, Hash]
|
|
100
|
+
def source(value, **options)
|
|
101
|
+
source_name = value.to_sym
|
|
102
|
+
return mapping_source(source_name, **options) if options.any? || MAPPING_SOURCE_KINDS.include?(source_name)
|
|
103
|
+
|
|
104
|
+
@params[:source] = source_name
|
|
105
|
+
end
|
|
106
|
+
|
|
46
107
|
# @param value [Integer] particle count or similar numeric parameter
|
|
47
108
|
# @return [Integer]
|
|
48
109
|
def count(value)
|
|
@@ -61,52 +122,265 @@ module Vizcore
|
|
|
61
122
|
@params[:font_size] = Integer(value)
|
|
62
123
|
end
|
|
63
124
|
|
|
125
|
+
# @param value [Numeric] extra spacing between text glyphs in pixels
|
|
126
|
+
# @return [Float]
|
|
127
|
+
def letter_spacing(value)
|
|
128
|
+
@params[:letter_spacing] = normalize_non_negative_param_number(value, :letter_spacing)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @param value [Symbol, String] text alignment (`left`, `center`, `right`)
|
|
132
|
+
# @return [Symbol]
|
|
133
|
+
def align(value)
|
|
134
|
+
alignment = value.to_sym
|
|
135
|
+
raise ArgumentError, "unsupported text align: #{value.inspect}" unless %i[left center right].include?(alignment)
|
|
136
|
+
|
|
137
|
+
@params[:align] = alignment
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @param value [String] text font family
|
|
141
|
+
# @return [String]
|
|
142
|
+
def font(value)
|
|
143
|
+
@params[:font] = value.to_s
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @param value [String] text fill color
|
|
147
|
+
# @return [String]
|
|
148
|
+
def fill(value)
|
|
149
|
+
@params[:color] = value.to_s
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# @param width [Numeric, nil] text stroke width in pixels
|
|
153
|
+
# @param color [String, nil] text stroke color
|
|
154
|
+
# @return [Hash]
|
|
155
|
+
def stroke(value = NO_ARGUMENT, width: nil, color: nil)
|
|
156
|
+
if @current_shape
|
|
157
|
+
@current_shape[:stroke] = normalize_non_negative_param_number(value, :stroke) unless value.equal?(NO_ARGUMENT)
|
|
158
|
+
@current_shape[:stroke_width] = normalize_non_negative_param_number(width, :stroke_width) unless width.nil?
|
|
159
|
+
@current_shape[:stroke_color] = color.to_s unless color.nil?
|
|
160
|
+
return @current_shape
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
@params[:stroke_width] = normalize_non_negative_param_number(width, :stroke_width) unless width.nil?
|
|
164
|
+
@params[:stroke_color] = color.to_s unless color.nil?
|
|
165
|
+
@params
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @param color [String, nil] text shadow color
|
|
169
|
+
# @param blur [Numeric, nil] text shadow blur in pixels
|
|
170
|
+
# @return [Hash]
|
|
171
|
+
def shadow(color: nil, blur: nil)
|
|
172
|
+
@params[:shadow_color] = color.to_s unless color.nil?
|
|
173
|
+
@params[:shadow_blur] = normalize_non_negative_param_number(blur, :shadow_blur) unless blur.nil?
|
|
174
|
+
@params
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# @param value [Symbol, String] layer compositing mode
|
|
178
|
+
# @return [Symbol]
|
|
179
|
+
def blend(value)
|
|
180
|
+
@params[:blend] = value.to_sym
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Store an ordered color palette for this layer.
|
|
184
|
+
#
|
|
185
|
+
# @param colors [Array<String, Array<String>>] color values such as "#00ffff"
|
|
186
|
+
# @raise [ArgumentError] when no non-blank colors are supplied
|
|
187
|
+
# @return [Array<String>]
|
|
188
|
+
def palette(*colors)
|
|
189
|
+
@params[:palette] = normalize_palette(colors)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Apply a named style by merging its params into this layer.
|
|
193
|
+
#
|
|
194
|
+
# @param name [Symbol, String] style identifier
|
|
195
|
+
# @raise [ArgumentError] when the style is unknown
|
|
196
|
+
# @return [Hash] applied style params
|
|
197
|
+
def use_style(name)
|
|
198
|
+
style_name = name.to_sym
|
|
199
|
+
style_params = @styles.fetch(style_name) { raise ArgumentError, "unknown style: #{style_name}" }
|
|
200
|
+
@params.merge!(deep_dup(style_params))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Declare numeric metadata for a shader/layer parameter.
|
|
204
|
+
#
|
|
205
|
+
# @param name [Symbol, String] parameter name exposed as `u_param_<name>` for shaders
|
|
206
|
+
# @param default [Numeric, nil] default value stored in layer params
|
|
207
|
+
# @param range [Range, Array, nil] allowed numeric range
|
|
208
|
+
# @param min [Numeric, nil] allowed minimum when `range` is not used
|
|
209
|
+
# @param max [Numeric, nil] allowed maximum when `range` is not used
|
|
210
|
+
# @param step [Numeric, nil] preferred UI step
|
|
211
|
+
# @return [Hash]
|
|
212
|
+
def param(name, default: nil, range: nil, min: nil, max: nil, step: nil)
|
|
213
|
+
key = normalize_param_name(name)
|
|
214
|
+
range_min, range_max = normalize_range(range, context: "param")
|
|
215
|
+
min = range_min if min.nil?
|
|
216
|
+
max = range_max if max.nil?
|
|
217
|
+
|
|
218
|
+
metadata = { name: key }
|
|
219
|
+
metadata[:default] = normalize_param_number(default, :default) unless default.nil?
|
|
220
|
+
metadata[:min] = normalize_param_number(min, :min) unless min.nil?
|
|
221
|
+
metadata[:max] = normalize_param_number(max, :max) unless max.nil?
|
|
222
|
+
metadata[:step] = normalize_param_number(step, :step) unless step.nil?
|
|
223
|
+
validate_param_range!(metadata)
|
|
224
|
+
|
|
225
|
+
@params[key] = metadata[:default] if metadata.key?(:default)
|
|
226
|
+
@param_schema[key] = metadata
|
|
227
|
+
end
|
|
228
|
+
|
|
64
229
|
# Map analysis source(s) to layer parameter target(s).
|
|
65
230
|
#
|
|
66
|
-
# @param definition [Hash] mapping pairs
|
|
231
|
+
# @param definition [Hash, Symbol, String] mapping pairs or a single source
|
|
67
232
|
# @raise [ArgumentError] when the mapping is empty or invalid
|
|
68
233
|
# @return [void]
|
|
69
|
-
def map(definition)
|
|
70
|
-
|
|
234
|
+
def map(definition = nil, **options, &block)
|
|
235
|
+
definition, options = normalize_shape_mapping(definition, options) if @shape_target_prefix
|
|
236
|
+
|
|
237
|
+
if options.key?(:to)
|
|
238
|
+
transform_options = options.dup
|
|
239
|
+
to = transform_options.delete(:to)
|
|
240
|
+
transform_options = evaluate_transform_block(transform_options, &block) if block
|
|
241
|
+
@mappings << build_mapping(
|
|
242
|
+
source: normalize_source(definition),
|
|
243
|
+
target: to,
|
|
244
|
+
transform: normalize_transform(**transform_options)
|
|
245
|
+
)
|
|
246
|
+
return
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
mapping = definition.nil? ? options : Hash(definition)
|
|
71
250
|
raise ArgumentError, "map requires at least one mapping pair" if mapping.empty?
|
|
251
|
+
raise ArgumentError, "map block syntax supports one mapping pair" if block && mapping.length != 1
|
|
72
252
|
|
|
73
253
|
mapping.each do |source, target|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
254
|
+
target_name, transform = normalize_target(target)
|
|
255
|
+
transform = normalize_transform(**evaluate_transform_block(transform, &block)) if block
|
|
256
|
+
@mappings << build_mapping(source: normalize_source(source), target: target_name, transform: transform)
|
|
78
257
|
end
|
|
79
258
|
end
|
|
80
259
|
|
|
260
|
+
# High-level mapping DSL for describing audio reactions inside a layer.
|
|
261
|
+
#
|
|
262
|
+
# @param source_value [Hash, Symbol, String] analysis source descriptor
|
|
263
|
+
# @yield Reaction block with `change` and `trigger`
|
|
264
|
+
# @raise [ArgumentError] when no reaction block is provided
|
|
265
|
+
# @return [void]
|
|
266
|
+
def react_to(source_value, &block)
|
|
267
|
+
raise ArgumentError, "react_to requires a block" unless block
|
|
268
|
+
|
|
269
|
+
source_descriptor = normalize_source(source_value)
|
|
270
|
+
reaction = ReactionBuilder.new(
|
|
271
|
+
mapping_factory: lambda do |target, transform_options|
|
|
272
|
+
build_mapping(
|
|
273
|
+
source: source_descriptor,
|
|
274
|
+
target: target,
|
|
275
|
+
transform: normalize_transform(**transform_options)
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
)
|
|
279
|
+
@mappings.concat(reaction.evaluate(&block))
|
|
280
|
+
end
|
|
281
|
+
|
|
81
282
|
# @return [Hash] source descriptor for overall amplitude
|
|
82
283
|
def amplitude
|
|
83
|
-
|
|
284
|
+
mapping_source(:amplitude)
|
|
84
285
|
end
|
|
85
286
|
|
|
86
287
|
# @param name [Symbol, String] band key (`sub`, `low`, `mid`, `high`)
|
|
87
288
|
# @return [Hash] source descriptor for a frequency band
|
|
88
289
|
def frequency_band(name)
|
|
89
|
-
|
|
290
|
+
mapping_source(:frequency_band, band: name.to_sym)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# @return [Hash] source descriptor for the sub-bass frequency band
|
|
294
|
+
def sub
|
|
295
|
+
frequency_band(:sub)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# @return [Hash] source descriptor for the low/bass frequency band
|
|
299
|
+
def low
|
|
300
|
+
frequency_band(:low)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# @return [Hash] source descriptor for the low/bass frequency band
|
|
304
|
+
def bass
|
|
305
|
+
frequency_band(:low)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# @return [Hash] source descriptor for the mid frequency band
|
|
309
|
+
def mid
|
|
310
|
+
frequency_band(:mid)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# @return [Hash] source descriptor for the high frequency band
|
|
314
|
+
def high
|
|
315
|
+
frequency_band(:high)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# @return [Hash] source descriptor for the high/treble frequency band
|
|
319
|
+
def treble
|
|
320
|
+
frequency_band(:high)
|
|
90
321
|
end
|
|
91
322
|
|
|
92
323
|
# @return [Hash] source descriptor for FFT spectrum array
|
|
93
324
|
def fft_spectrum
|
|
94
|
-
|
|
325
|
+
mapping_source(:fft_spectrum)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# @param band [Symbol, String, nil] optional band-specific onset key
|
|
329
|
+
# @return [Hash] source descriptor for positive audio feature changes
|
|
330
|
+
def onset(band = nil)
|
|
331
|
+
options = band.nil? ? {} : { band: band.to_sym }
|
|
332
|
+
mapping_source(:onset, **options)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# @return [Hash] source descriptor for low-band percussive confidence
|
|
336
|
+
def kick(value = NO_ARGUMENT)
|
|
337
|
+
return @params[:kick] = value unless value.equal?(NO_ARGUMENT)
|
|
338
|
+
|
|
339
|
+
mapping_source(:kick)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# @return [Hash] source descriptor for mid-band percussive confidence
|
|
343
|
+
def snare(value = NO_ARGUMENT)
|
|
344
|
+
return @params[:snare] = value unless value.equal?(NO_ARGUMENT)
|
|
345
|
+
|
|
346
|
+
mapping_source(:snare)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# @return [Hash] source descriptor for high-band percussive confidence
|
|
350
|
+
def hihat(value = NO_ARGUMENT)
|
|
351
|
+
return @params[:hihat] = value unless value.equal?(NO_ARGUMENT)
|
|
352
|
+
|
|
353
|
+
mapping_source(:hihat)
|
|
95
354
|
end
|
|
96
355
|
|
|
97
356
|
# @return [Hash] source descriptor for beat trigger
|
|
98
357
|
def beat?
|
|
99
|
-
|
|
358
|
+
mapping_source(:beat)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# @return [Hash] source descriptor for beat trigger
|
|
362
|
+
def beat
|
|
363
|
+
beat?
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# @return [Hash] source descriptor for beat detector confidence
|
|
367
|
+
def beat_confidence
|
|
368
|
+
mapping_source(:beat_confidence)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# @return [Hash] source descriptor for beat pulse decay value
|
|
372
|
+
def beat_pulse
|
|
373
|
+
mapping_source(:beat_pulse)
|
|
100
374
|
end
|
|
101
375
|
|
|
102
376
|
# @return [Hash] source descriptor for beat counter
|
|
103
377
|
def beat_count
|
|
104
|
-
|
|
378
|
+
mapping_source(:beat_count)
|
|
105
379
|
end
|
|
106
380
|
|
|
107
381
|
# @return [Hash] source descriptor for estimated BPM
|
|
108
382
|
def bpm
|
|
109
|
-
|
|
383
|
+
mapping_source(:bpm)
|
|
110
384
|
end
|
|
111
385
|
|
|
112
386
|
# @return [Hash] serialized layer payload
|
|
@@ -118,6 +392,7 @@ module Vizcore
|
|
|
118
392
|
}
|
|
119
393
|
layer[:shader] = @shader if @shader
|
|
120
394
|
layer[:glsl] = @glsl if @glsl
|
|
395
|
+
layer[:param_schema] = @param_schema.values.map(&:dup) unless @param_schema.empty?
|
|
121
396
|
layer[:mappings] = @mappings.map { |mapping| mapping.dup } unless @mappings.empty?
|
|
122
397
|
layer
|
|
123
398
|
end
|
|
@@ -125,6 +400,11 @@ module Vizcore
|
|
|
125
400
|
# Stores dynamic one-argument setters into `params`.
|
|
126
401
|
# @api private
|
|
127
402
|
def method_missing(method_name, *args, &block)
|
|
403
|
+
if @current_shape && block.nil? && args.length == 1
|
|
404
|
+
@current_shape[method_name.to_sym] = args.first
|
|
405
|
+
return args.first
|
|
406
|
+
end
|
|
407
|
+
|
|
128
408
|
if block.nil? && args.length == 1
|
|
129
409
|
@params[method_name.to_sym] = args.first
|
|
130
410
|
return args.first
|
|
@@ -139,6 +419,65 @@ module Vizcore
|
|
|
139
419
|
|
|
140
420
|
private
|
|
141
421
|
|
|
422
|
+
def build_shape(kind, options, &block)
|
|
423
|
+
@type ||= :shape
|
|
424
|
+
shape = normalize_shape(kind, options)
|
|
425
|
+
@params[:shapes] ||= []
|
|
426
|
+
shape_index = @params[:shapes].length
|
|
427
|
+
@params[:shapes] << shape
|
|
428
|
+
|
|
429
|
+
with_shape_context(shape, shape_index) do
|
|
430
|
+
instance_eval(&block) if block
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
shape
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def normalize_shape(kind, options)
|
|
437
|
+
shape = { kind: kind.to_sym }
|
|
438
|
+
options.each do |key, value|
|
|
439
|
+
shape[key.to_sym] = value
|
|
440
|
+
end
|
|
441
|
+
shape
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def with_shape_context(shape, shape_index)
|
|
445
|
+
previous_shape = @current_shape
|
|
446
|
+
previous_prefix = @shape_target_prefix
|
|
447
|
+
@current_shape = shape
|
|
448
|
+
@shape_target_prefix = "shapes.#{shape_index}"
|
|
449
|
+
yield
|
|
450
|
+
ensure
|
|
451
|
+
@current_shape = previous_shape
|
|
452
|
+
@shape_target_prefix = previous_prefix
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def normalize_shape_mapping(definition, options)
|
|
456
|
+
if options.key?(:to)
|
|
457
|
+
prefixed_options = options.dup
|
|
458
|
+
prefixed_options[:to] = prefixed_shape_target(prefixed_options[:to])
|
|
459
|
+
return [definition, prefixed_options]
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
mapping = definition.nil? ? options : Hash(definition)
|
|
463
|
+
prefixed_mapping = mapping.each_with_object({}) do |(source, target), output|
|
|
464
|
+
output[source] = prefix_shape_target_value(target)
|
|
465
|
+
end
|
|
466
|
+
[prefixed_mapping, {}]
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def prefix_shape_target_value(target)
|
|
470
|
+
return prefixed_shape_target(target) unless target.is_a?(Hash)
|
|
471
|
+
|
|
472
|
+
target.merge(to: prefixed_shape_target(target.fetch(:to)))
|
|
473
|
+
rescue KeyError
|
|
474
|
+
target
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def prefixed_shape_target(target)
|
|
478
|
+
:"#{@shape_target_prefix}.#{target}"
|
|
479
|
+
end
|
|
480
|
+
|
|
142
481
|
def resolved_type
|
|
143
482
|
return @type if @type
|
|
144
483
|
return :shader if @shader || @glsl
|
|
@@ -146,17 +485,24 @@ module Vizcore
|
|
|
146
485
|
:geometry
|
|
147
486
|
end
|
|
148
487
|
|
|
488
|
+
def shader_path?(value)
|
|
489
|
+
return false if value.is_a?(Symbol)
|
|
490
|
+
|
|
491
|
+
path = value.to_s
|
|
492
|
+
%w[.frag .glsl].include?(File.extname(path).downcase) || path.include?("/")
|
|
493
|
+
end
|
|
494
|
+
|
|
149
495
|
def normalize_source(source_value)
|
|
150
496
|
case source_value
|
|
151
497
|
when Hash
|
|
152
498
|
kind = source_value[:kind] || source_value["kind"]
|
|
153
499
|
raise ArgumentError, "mapping source hash must contain :kind" unless kind
|
|
154
500
|
|
|
155
|
-
|
|
501
|
+
mapping_source(kind.to_sym, **normalize_source_options(source_value))
|
|
156
502
|
when Symbol
|
|
157
|
-
|
|
503
|
+
mapping_source(source_value)
|
|
158
504
|
when String
|
|
159
|
-
|
|
505
|
+
mapping_source(source_value.to_sym)
|
|
160
506
|
else
|
|
161
507
|
raise ArgumentError, "unsupported mapping source: #{source_value.inspect}"
|
|
162
508
|
end
|
|
@@ -171,7 +517,130 @@ module Vizcore
|
|
|
171
517
|
end
|
|
172
518
|
end
|
|
173
519
|
|
|
174
|
-
def
|
|
520
|
+
def normalize_target(target)
|
|
521
|
+
return [target.to_sym, {}] unless target.is_a?(Hash)
|
|
522
|
+
|
|
523
|
+
values = target.each_with_object({}) { |(key, value), output| output[key.to_sym] = value }
|
|
524
|
+
to = values.delete(:to)
|
|
525
|
+
raise ArgumentError, "mapping target hash must contain :to" unless to
|
|
526
|
+
|
|
527
|
+
[to.to_sym, normalize_transform(**values)]
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def build_mapping(source:, target:, transform: {})
|
|
531
|
+
output = { source: source, target: target.to_sym }
|
|
532
|
+
output[:transform] = transform unless transform.empty?
|
|
533
|
+
output
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def deep_dup(value)
|
|
537
|
+
case value
|
|
538
|
+
when Hash
|
|
539
|
+
value.each_with_object({}) do |(key, entry), output|
|
|
540
|
+
output[key] = deep_dup(entry)
|
|
541
|
+
end
|
|
542
|
+
when Array
|
|
543
|
+
value.map { |entry| deep_dup(entry) }
|
|
544
|
+
else
|
|
545
|
+
value
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def evaluate_transform_block(initial_options, &block)
|
|
550
|
+
MappingTransformBuilder.new(initial_options).evaluate(&block).to_h
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def normalize_param_name(name)
|
|
554
|
+
key = name.to_s.strip
|
|
555
|
+
raise ArgumentError, "param name is required" if key.empty?
|
|
556
|
+
|
|
557
|
+
key.to_sym
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def normalize_palette(colors)
|
|
561
|
+
values = colors.flatten.map { |color| color.to_s.strip }.reject(&:empty?)
|
|
562
|
+
raise ArgumentError, "layer #{@name} palette requires at least one color" if values.empty?
|
|
563
|
+
|
|
564
|
+
values
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def normalize_param_number(value, name)
|
|
568
|
+
Float(value)
|
|
569
|
+
rescue ArgumentError, TypeError
|
|
570
|
+
raise ArgumentError, "param #{name} must be numeric"
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def validate_param_range!(metadata)
|
|
574
|
+
return unless metadata.key?(:min) && metadata.key?(:max)
|
|
575
|
+
return if metadata[:min] <= metadata[:max]
|
|
576
|
+
|
|
577
|
+
raise ArgumentError, "param min must be less than or equal to max"
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def normalize_transform(gain: nil, range: nil, min: nil, max: nil, curve: nil, attack: nil, release: nil, deadzone: nil)
|
|
581
|
+
range_min, range_max = normalize_range(range, context: "mapping")
|
|
582
|
+
min = range_min if min.nil?
|
|
583
|
+
max = range_max if max.nil?
|
|
584
|
+
|
|
585
|
+
output = {}
|
|
586
|
+
output[:deadzone] = normalize_non_negative_float(deadzone, :deadzone) unless deadzone.nil?
|
|
587
|
+
output[:gain] = normalize_float(gain, :gain) unless gain.nil?
|
|
588
|
+
output[:min] = normalize_float(min, :min) unless min.nil?
|
|
589
|
+
output[:max] = normalize_float(max, :max) unless max.nil?
|
|
590
|
+
output[:curve] = normalize_curve(curve) unless curve.nil?
|
|
591
|
+
output[:attack] = clamp(normalize_float(attack, :attack), 0.0, 1.0) unless attack.nil?
|
|
592
|
+
output[:release] = clamp(normalize_float(release, :release), 0.0, 1.0) unless release.nil?
|
|
593
|
+
output
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def normalize_range(value, context:)
|
|
597
|
+
return [nil, nil] if value.nil?
|
|
598
|
+
|
|
599
|
+
if value.is_a?(Range)
|
|
600
|
+
return [value.begin, value.end]
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
if value.is_a?(Array) && value.length == 2
|
|
604
|
+
return value
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
raise ArgumentError, "#{context} range must be a Range or two-element Array"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def normalize_float(value, name)
|
|
611
|
+
Float(value)
|
|
612
|
+
rescue ArgumentError, TypeError
|
|
613
|
+
raise ArgumentError, "mapping #{name} must be numeric"
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def normalize_non_negative_float(value, name)
|
|
617
|
+
numeric = normalize_float(value, name)
|
|
618
|
+
raise ArgumentError, "mapping #{name} must be non-negative" if numeric.negative?
|
|
619
|
+
|
|
620
|
+
numeric
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def normalize_non_negative_param_number(value, name)
|
|
624
|
+
numeric = Float(value)
|
|
625
|
+
raise ArgumentError, "#{name} must be non-negative" if numeric.negative?
|
|
626
|
+
|
|
627
|
+
numeric
|
|
628
|
+
rescue ArgumentError, TypeError
|
|
629
|
+
raise ArgumentError, "#{name} must be numeric"
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def normalize_curve(value)
|
|
633
|
+
curve = value.to_sym
|
|
634
|
+
return curve if %i[linear sqrt square ease_out].include?(curve)
|
|
635
|
+
|
|
636
|
+
raise ArgumentError, "unsupported mapping curve: #{value.inspect}"
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def clamp(value, min, max)
|
|
640
|
+
[[value, min].max, max].min
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def mapping_source(kind, **options)
|
|
175
644
|
{
|
|
176
645
|
kind: kind.to_sym,
|
|
177
646
|
**options
|