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
|
@@ -19,6 +19,7 @@ module Vizcore
|
|
|
19
19
|
@midi_maps = normalize_midi_maps(midi_maps)
|
|
20
20
|
@scenes = normalize_scenes(scenes)
|
|
21
21
|
@globals = normalize_globals(globals) unless globals.nil?
|
|
22
|
+
@cc_state = {}
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
# @return [Hash] mutable global parameter snapshot
|
|
@@ -32,8 +33,11 @@ module Vizcore
|
|
|
32
33
|
@midi_maps.each_with_object([]) do |mapping, actions|
|
|
33
34
|
next unless mapping_match?(mapping[:trigger], event)
|
|
34
35
|
|
|
36
|
+
value = event_value(event, mapping[:trigger])
|
|
37
|
+
next if value.nil?
|
|
38
|
+
|
|
35
39
|
context = ActionContext.new(scenes: @scenes, globals: @globals)
|
|
36
|
-
invoke_action_block(context, mapping[:action],
|
|
40
|
+
invoke_action_block(context, mapping[:action], value)
|
|
37
41
|
actions.concat(context.actions)
|
|
38
42
|
end
|
|
39
43
|
end
|
|
@@ -73,6 +77,8 @@ module Vizcore
|
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
def mapping_match?(trigger, event)
|
|
80
|
+
return false unless channel_match?(trigger, event)
|
|
81
|
+
|
|
76
82
|
if trigger.key?(:note)
|
|
77
83
|
event.type == :note_on && event.data1 == trigger[:note].to_i
|
|
78
84
|
elsif trigger.key?(:cc)
|
|
@@ -84,8 +90,13 @@ module Vizcore
|
|
|
84
90
|
end
|
|
85
91
|
end
|
|
86
92
|
|
|
87
|
-
def
|
|
88
|
-
|
|
93
|
+
def channel_match?(trigger, event)
|
|
94
|
+
return true unless trigger.key?(:channel)
|
|
95
|
+
|
|
96
|
+
event.channel.to_i == trigger[:channel].to_i
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def invoke_action_block(context, action, value)
|
|
89
100
|
if action.arity.zero?
|
|
90
101
|
context.instance_exec(&action)
|
|
91
102
|
else
|
|
@@ -94,8 +105,10 @@ module Vizcore
|
|
|
94
105
|
end
|
|
95
106
|
|
|
96
107
|
def event_value(event, trigger)
|
|
97
|
-
if trigger.key?(:note)
|
|
108
|
+
if trigger.key?(:note)
|
|
98
109
|
event.data2.to_i.clamp(0, 127)
|
|
110
|
+
elsif trigger.key?(:cc)
|
|
111
|
+
cc_event_value(event, trigger)
|
|
99
112
|
elsif trigger.key?(:pc)
|
|
100
113
|
event.data1.to_i.clamp(0, 127)
|
|
101
114
|
else
|
|
@@ -103,6 +116,78 @@ module Vizcore
|
|
|
103
116
|
end
|
|
104
117
|
end
|
|
105
118
|
|
|
119
|
+
def cc_event_value(event, trigger)
|
|
120
|
+
raw = event.data2.to_i.clamp(0, 127)
|
|
121
|
+
state = (@cc_state[state_key(trigger)] ||= {})
|
|
122
|
+
value = trigger[:relative] ? relative_cc_delta(raw) : raw
|
|
123
|
+
return nil if pickup_blocked?(raw, state, trigger)
|
|
124
|
+
return nil if within_deadband?(value, state, trigger)
|
|
125
|
+
|
|
126
|
+
value = smooth_value(value, state, trigger)
|
|
127
|
+
state[:last_raw] = raw unless trigger[:relative]
|
|
128
|
+
state[:last_value] = value
|
|
129
|
+
value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def relative_cc_delta(raw)
|
|
133
|
+
return raw if raw.between?(1, 63)
|
|
134
|
+
return raw - 128 if raw.between?(65, 127)
|
|
135
|
+
|
|
136
|
+
0
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def pickup_blocked?(raw, state, trigger)
|
|
140
|
+
return false unless trigger[:pickup]
|
|
141
|
+
return false unless trigger.key?(:cc)
|
|
142
|
+
return false if state[:pickup_synced]
|
|
143
|
+
|
|
144
|
+
return false if trigger[:relative]
|
|
145
|
+
|
|
146
|
+
reference = state[:pickup_reference_raw]
|
|
147
|
+
unless reference
|
|
148
|
+
state[:pickup_reference_raw] = raw
|
|
149
|
+
return true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
tolerance = trigger[:deadband] || 1
|
|
153
|
+
if (raw - reference).abs <= tolerance
|
|
154
|
+
state[:pickup_synced] = true
|
|
155
|
+
state[:pickup_reference_raw] = nil
|
|
156
|
+
return false
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def within_deadband?(value, state, trigger)
|
|
163
|
+
return false unless trigger.key?(:deadband)
|
|
164
|
+
|
|
165
|
+
deadband = Float(trigger[:deadband])
|
|
166
|
+
if trigger[:relative]
|
|
167
|
+
value.abs <= deadband
|
|
168
|
+
elsif state.key?(:last_raw)
|
|
169
|
+
(value - state[:last_raw].to_f).abs <= deadband
|
|
170
|
+
else
|
|
171
|
+
false
|
|
172
|
+
end
|
|
173
|
+
rescue ArgumentError, TypeError
|
|
174
|
+
false
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def smooth_value(value, state, trigger)
|
|
178
|
+
return value unless trigger.key?(:smooth)
|
|
179
|
+
return value unless state.key?(:last_value)
|
|
180
|
+
|
|
181
|
+
alpha = Float(trigger[:smooth]).clamp(0.0, 1.0)
|
|
182
|
+
state[:last_value].to_f + ((value - state[:last_value].to_f) * alpha)
|
|
183
|
+
rescue ArgumentError, TypeError
|
|
184
|
+
value
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def state_key(trigger)
|
|
188
|
+
[trigger[:channel], trigger[:cc]]
|
|
189
|
+
end
|
|
190
|
+
|
|
106
191
|
def symbolize_hash(value)
|
|
107
192
|
Hash(value).each_with_object({}) do |(key, entry), output|
|
|
108
193
|
output[key.to_sym] = entry
|
|
@@ -112,16 +197,7 @@ module Vizcore
|
|
|
112
197
|
end
|
|
113
198
|
|
|
114
199
|
def deep_dup(value)
|
|
115
|
-
|
|
116
|
-
when Hash
|
|
117
|
-
value.each_with_object({}) do |(key, entry), output|
|
|
118
|
-
output[key] = deep_dup(entry)
|
|
119
|
-
end
|
|
120
|
-
when Array
|
|
121
|
-
value.map { |entry| deep_dup(entry) }
|
|
122
|
-
else
|
|
123
|
-
value
|
|
124
|
-
end
|
|
200
|
+
Vizcore::DeepCopy.copy(value)
|
|
125
201
|
end
|
|
126
202
|
|
|
127
203
|
# Runtime DSL context used while executing one `midi_map` action block.
|
|
@@ -129,6 +205,7 @@ module Vizcore
|
|
|
129
205
|
class ActionContext
|
|
130
206
|
# Collected runtime actions emitted by DSL calls.
|
|
131
207
|
attr_reader :actions
|
|
208
|
+
attr_reader :unknown_scene_names
|
|
132
209
|
|
|
133
210
|
# @param scenes [Hash]
|
|
134
211
|
# @param globals [Hash]
|
|
@@ -136,6 +213,7 @@ module Vizcore
|
|
|
136
213
|
@scenes = scenes
|
|
137
214
|
@globals = globals
|
|
138
215
|
@actions = []
|
|
216
|
+
@unknown_scene_names = []
|
|
139
217
|
end
|
|
140
218
|
|
|
141
219
|
# @param name [Symbol, String]
|
|
@@ -143,7 +221,11 @@ module Vizcore
|
|
|
143
221
|
# @return [void]
|
|
144
222
|
def switch_scene(name, effect: nil)
|
|
145
223
|
scene = @scenes[name.to_sym]
|
|
146
|
-
|
|
224
|
+
unless scene
|
|
225
|
+
unknown = name.to_s
|
|
226
|
+
@unknown_scene_names << unknown unless @unknown_scene_names.include?(unknown)
|
|
227
|
+
return
|
|
228
|
+
end
|
|
147
229
|
|
|
148
230
|
@actions << {
|
|
149
231
|
type: :switch_scene,
|
|
@@ -155,6 +237,22 @@ module Vizcore
|
|
|
155
237
|
}
|
|
156
238
|
end
|
|
157
239
|
|
|
240
|
+
# Advance to the next scene in runtime scene order.
|
|
241
|
+
#
|
|
242
|
+
# @param effect [Hash, nil]
|
|
243
|
+
# @return [void]
|
|
244
|
+
def next_scene(effect: nil)
|
|
245
|
+
@actions << { type: :next_scene, effect: deep_dup(effect) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Move to the previous scene in runtime scene order.
|
|
249
|
+
#
|
|
250
|
+
# @param effect [Hash, nil]
|
|
251
|
+
# @return [void]
|
|
252
|
+
def previous_scene(effect: nil)
|
|
253
|
+
@actions << { type: :previous_scene, effect: deep_dup(effect) }
|
|
254
|
+
end
|
|
255
|
+
|
|
158
256
|
# @param key [Symbol, String]
|
|
159
257
|
# @param value [Object]
|
|
160
258
|
# @return [void]
|
|
@@ -168,19 +266,117 @@ module Vizcore
|
|
|
168
266
|
}
|
|
169
267
|
end
|
|
170
268
|
|
|
269
|
+
# @param control [Symbol, String]
|
|
270
|
+
# @param value [Boolean, nil] target value; nil defaults to true for direct calls
|
|
271
|
+
# @param fade [Numeric, nil] optional seconds for transition to `value == true`
|
|
272
|
+
# @param release [Numeric, nil] optional seconds for transition to `value == false`
|
|
273
|
+
# @return [void]
|
|
274
|
+
def live_control(control, value = nil, fade: nil, release: nil, color: nil)
|
|
275
|
+
state = normalize_live_control_state(value)
|
|
276
|
+
state[:fade] = normalize_control_transition(fade)
|
|
277
|
+
state[:release] = normalize_control_transition(release)
|
|
278
|
+
state[:color] = normalize_control_color(color)
|
|
279
|
+
state.delete(:fade) if state[:fade].nil?
|
|
280
|
+
state.delete(:release) if state[:release].nil?
|
|
281
|
+
state.delete(:color) if state[:color].nil?
|
|
282
|
+
|
|
283
|
+
@actions << {
|
|
284
|
+
type: :live_control,
|
|
285
|
+
control: control.to_s,
|
|
286
|
+
**state
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# @param value [Boolean, nil]
|
|
291
|
+
# @return [void]
|
|
292
|
+
def blackout(value = nil, fade: nil, release: nil, color: nil)
|
|
293
|
+
live_control(:blackout, value, fade: fade, release: release, color: color)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# @param value [Boolean, nil]
|
|
297
|
+
# @return [void]
|
|
298
|
+
def freeze(value = nil, fade: nil, release: nil)
|
|
299
|
+
live_control(:freeze, value, fade: fade, release: release)
|
|
300
|
+
end
|
|
301
|
+
|
|
171
302
|
private
|
|
172
303
|
|
|
304
|
+
def normalize_live_control_state(value)
|
|
305
|
+
if value.is_a?(Hash)
|
|
306
|
+
state = value.transform_keys(&:to_sym)
|
|
307
|
+
enabled = state.key?(:value) ? state[:value] : true
|
|
308
|
+
return {
|
|
309
|
+
value: !!enabled,
|
|
310
|
+
fade: normalize_control_transition(state[:fade]),
|
|
311
|
+
release: normalize_control_transition(state[:release]),
|
|
312
|
+
color: normalize_control_color(state[:color]),
|
|
313
|
+
}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
{ value: !!(value.nil? || value) }
|
|
317
|
+
end
|
|
318
|
+
|
|
173
319
|
def deep_dup(value)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
320
|
+
Vizcore::DeepCopy.copy(value)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def normalize_control_transition(value)
|
|
324
|
+
return nil if value.nil?
|
|
325
|
+
|
|
326
|
+
numeric = Float(value)
|
|
327
|
+
return nil if numeric.negative? || !numeric.finite?
|
|
328
|
+
|
|
329
|
+
numeric
|
|
330
|
+
rescue ArgumentError, TypeError
|
|
331
|
+
nil
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def normalize_control_color(value)
|
|
335
|
+
return nil if value.nil?
|
|
336
|
+
|
|
337
|
+
if value.is_a?(Array)
|
|
338
|
+
return nil unless (3..4).cover?(value.length)
|
|
339
|
+
|
|
340
|
+
channels = Array(value).map { |entry| Float(entry, exception: false) }
|
|
341
|
+
return nil if channels.include?(nil)
|
|
342
|
+
|
|
343
|
+
rgb = channels.take(3)
|
|
344
|
+
alpha = channels[3]
|
|
345
|
+
normalized_rgb = if rgb.all? { |channel| channel.between?(0.0, 1.0) }
|
|
346
|
+
rgb
|
|
347
|
+
else
|
|
348
|
+
rgb.map { |channel| channel / 255.0 }
|
|
178
349
|
end
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
else
|
|
182
|
-
value
|
|
350
|
+
normalized = normalized_rgb.map { |channel| [0.0, [1.0, channel].min].max }
|
|
351
|
+
return alpha.nil? ? normalized : normalized + [normalize_control_alpha(alpha)]
|
|
183
352
|
end
|
|
353
|
+
|
|
354
|
+
raw = value.to_s.strip
|
|
355
|
+
match = raw.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/)
|
|
356
|
+
return nil unless match
|
|
357
|
+
|
|
358
|
+
raw_hex = match[1]
|
|
359
|
+
hex = raw_hex.length == 3 || raw_hex.length == 4 ? raw_hex.chars.map { |char| "#{char}#{char}" }.join("") : raw_hex
|
|
360
|
+
|
|
361
|
+
[
|
|
362
|
+
Integer("0x#{hex[0, 2]}", 16),
|
|
363
|
+
Integer("0x#{hex[2, 2]}", 16),
|
|
364
|
+
Integer("0x#{hex[4, 2]}", 16),
|
|
365
|
+
Integer("0x#{hex[6, 2]}", 16)
|
|
366
|
+
].take(raw_hex.length > 4 ? 4 : 3).map { |channel| [0.0, [1.0, channel / 255.0].min].max }
|
|
367
|
+
rescue ArgumentError, TypeError
|
|
368
|
+
nil
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def normalize_control_alpha(value)
|
|
372
|
+
return nil if value.nil?
|
|
373
|
+
|
|
374
|
+
alpha = Float(value, exception: false)
|
|
375
|
+
return nil if alpha.nil?
|
|
376
|
+
|
|
377
|
+
return [0.0, [1.0, alpha].min].max if alpha.between?(0.0, 1.0)
|
|
378
|
+
|
|
379
|
+
[0.0, [1.0, alpha / 255.0].min].max
|
|
184
380
|
end
|
|
185
381
|
end
|
|
186
382
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "layer_builder"
|
|
4
4
|
require_relative "layer_group_builder"
|
|
5
|
+
require_relative "style_builder"
|
|
6
|
+
require_relative "../deep_copy"
|
|
5
7
|
|
|
6
8
|
module Vizcore
|
|
7
9
|
module DSL
|
|
@@ -10,11 +12,15 @@ module Vizcore
|
|
|
10
12
|
# @param name [Symbol, String] scene identifier
|
|
11
13
|
# @param styles [Hash] reusable layer parameter styles
|
|
12
14
|
# @param themes [Hash] reusable scene-wide layer parameter themes
|
|
15
|
+
# @param mapping_presets [Hash] reusable mapping presets
|
|
13
16
|
# @param layers [Array<Hash>] initial layer definitions
|
|
14
|
-
|
|
17
|
+
# @param strict [Boolean] true when unknown layer params should fail
|
|
18
|
+
def initialize(name:, styles: {}, themes: {}, mapping_presets: {}, layers: [], strict: false)
|
|
15
19
|
@name = name.to_sym
|
|
16
20
|
@styles = styles
|
|
17
21
|
@themes = themes
|
|
22
|
+
@mapping_presets = mapping_presets
|
|
23
|
+
@strict = !!strict
|
|
18
24
|
@theme_name = nil
|
|
19
25
|
@theme_params = {}
|
|
20
26
|
@layers = layers.map { |layer| deep_dup(layer) }
|
|
@@ -35,18 +41,83 @@ module Vizcore
|
|
|
35
41
|
# @yield Layer definition block
|
|
36
42
|
# @return [void]
|
|
37
43
|
def layer(name, &block)
|
|
38
|
-
builder = LayerBuilder.new(name: name, styles: @styles, defaults: @theme_params)
|
|
44
|
+
builder = LayerBuilder.new(name: name, styles: @styles, mapping_presets: @mapping_presets, defaults: @theme_params, strict: @strict)
|
|
39
45
|
builder.evaluate(&block)
|
|
40
46
|
@layers << builder.to_h
|
|
41
47
|
end
|
|
42
48
|
|
|
49
|
+
# Set defaults applied to every layer in this scene.
|
|
50
|
+
#
|
|
51
|
+
# @param params [Hash] layer params
|
|
52
|
+
# @yield optional defaults block using style-like setters
|
|
53
|
+
# @return [Hash]
|
|
54
|
+
def scene_defaults(**params, &block)
|
|
55
|
+
defaults = params.each_with_object({}) { |(key, value), output| output[key.to_sym] = value }
|
|
56
|
+
if block
|
|
57
|
+
block_defaults = StyleBuilder.new(name: :scene_defaults, kind: "scene_defaults").evaluate(&block).to_h[:params]
|
|
58
|
+
defaults.merge!(block_defaults)
|
|
59
|
+
end
|
|
60
|
+
raise ArgumentError, "scene_defaults requires at least one parameter" if defaults.empty?
|
|
61
|
+
|
|
62
|
+
@theme_params = deep_dup(@theme_params).merge(defaults)
|
|
63
|
+
@layers = @layers.map { |layer| apply_theme_defaults(layer, defaults) }
|
|
64
|
+
deep_dup(@theme_params)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Remove an inherited or previously declared layer by name.
|
|
68
|
+
#
|
|
69
|
+
# @param name [Symbol, String]
|
|
70
|
+
# @return [Hash] removed layer definition
|
|
71
|
+
def remove_layer(name)
|
|
72
|
+
index = layer_index!(name)
|
|
73
|
+
@layers.delete_at(index)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Replace an inherited or previously declared layer while preserving order.
|
|
77
|
+
#
|
|
78
|
+
# @param name [Symbol, String]
|
|
79
|
+
# @yield Layer definition block
|
|
80
|
+
# @return [Hash] replacement layer definition
|
|
81
|
+
def replace_layer(name, &block)
|
|
82
|
+
index = layer_index!(name)
|
|
83
|
+
builder = LayerBuilder.new(
|
|
84
|
+
name: name,
|
|
85
|
+
styles: @styles,
|
|
86
|
+
mapping_presets: @mapping_presets,
|
|
87
|
+
defaults: @theme_params,
|
|
88
|
+
strict: @strict
|
|
89
|
+
)
|
|
90
|
+
builder.evaluate(&block)
|
|
91
|
+
@layers[index] = builder.to_h
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Override params on an existing layer without changing its type/shader.
|
|
95
|
+
#
|
|
96
|
+
# @param name [Symbol, String]
|
|
97
|
+
# @param params [Hash]
|
|
98
|
+
# @yield optional style-like param block
|
|
99
|
+
# @return [Hash] updated layer definition
|
|
100
|
+
def override_layer(name, **params, &block)
|
|
101
|
+
index = layer_index!(name)
|
|
102
|
+
overrides = params.each_with_object({}) { |(key, value), output| output[key.to_sym] = value }
|
|
103
|
+
if block
|
|
104
|
+
block_overrides = StyleBuilder.new(name: name, kind: "override_layer").evaluate(&block).to_h[:params]
|
|
105
|
+
overrides.merge!(block_overrides)
|
|
106
|
+
end
|
|
107
|
+
raise ArgumentError, "override_layer #{name} requires at least one parameter" if overrides.empty?
|
|
108
|
+
|
|
109
|
+
layer = deep_dup(@layers[index])
|
|
110
|
+
layer[:params] = Hash(layer[:params] || {}).merge(overrides)
|
|
111
|
+
@layers[index] = layer
|
|
112
|
+
end
|
|
113
|
+
|
|
43
114
|
# Define a related group of layers with shared params.
|
|
44
115
|
#
|
|
45
116
|
# @param name [Symbol, String] group identifier
|
|
46
117
|
# @yield Layer group definition block
|
|
47
118
|
# @return [void]
|
|
48
119
|
def group(name, &block)
|
|
49
|
-
builder = LayerGroupBuilder.new(name: name, styles: @styles, defaults: @theme_params)
|
|
120
|
+
builder = LayerGroupBuilder.new(name: name, styles: @styles, mapping_presets: @mapping_presets, defaults: @theme_params, strict: @strict)
|
|
50
121
|
builder.evaluate(&block)
|
|
51
122
|
@layers.concat(builder.to_a)
|
|
52
123
|
end
|
|
@@ -83,17 +154,16 @@ module Vizcore
|
|
|
83
154
|
themed_layer
|
|
84
155
|
end
|
|
85
156
|
|
|
157
|
+
def layer_index!(name)
|
|
158
|
+
normalized = name.to_sym
|
|
159
|
+
index = @layers.index { |layer| layer[:name]&.to_sym == normalized }
|
|
160
|
+
raise ArgumentError, "unknown layer: #{normalized}" unless index
|
|
161
|
+
|
|
162
|
+
index
|
|
163
|
+
end
|
|
164
|
+
|
|
86
165
|
def deep_dup(value)
|
|
87
|
-
|
|
88
|
-
when Hash
|
|
89
|
-
value.each_with_object({}) do |(key, entry), output|
|
|
90
|
-
output[key] = deep_dup(entry)
|
|
91
|
-
end
|
|
92
|
-
when Array
|
|
93
|
-
value.map { |entry| deep_dup(entry) }
|
|
94
|
-
else
|
|
95
|
-
value
|
|
96
|
-
end
|
|
166
|
+
Vizcore::DeepCopy.copy(value)
|
|
97
167
|
end
|
|
98
168
|
end
|
|
99
169
|
end
|
|
@@ -116,16 +116,7 @@ module Vizcore
|
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
def deep_dup(value)
|
|
119
|
-
|
|
120
|
-
when Hash
|
|
121
|
-
value.each_with_object({}) do |(key, entry), output|
|
|
122
|
-
output[key] = deep_dup(entry)
|
|
123
|
-
end
|
|
124
|
-
when Array
|
|
125
|
-
value.map { |entry| deep_dup(entry) }
|
|
126
|
-
else
|
|
127
|
-
value
|
|
128
|
-
end
|
|
119
|
+
Vizcore::DeepCopy.copy(value)
|
|
129
120
|
end
|
|
130
121
|
end
|
|
131
122
|
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require_relative "color_helpers"
|
|
2
3
|
|
|
3
4
|
module Vizcore
|
|
4
5
|
module DSL
|
|
5
6
|
# Collects reusable layer parameter presets for the `style` DSL.
|
|
6
7
|
class StyleBuilder
|
|
8
|
+
include ColorHelpers
|
|
9
|
+
|
|
7
10
|
# @param name [Symbol, String] style identifier
|
|
8
11
|
# @param kind [String] user-facing DSL kind for error messages
|
|
9
12
|
def initialize(name:, kind: "style")
|
|
@@ -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
|