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
|
@@ -4,6 +4,8 @@ module Vizcore
|
|
|
4
4
|
module DSL
|
|
5
5
|
# Collects block-style mapping transform options.
|
|
6
6
|
class MappingTransformBuilder
|
|
7
|
+
TRIGGER_MODES = %i[continuous trigger].freeze
|
|
8
|
+
|
|
7
9
|
# @param initial [Hash]
|
|
8
10
|
def initialize(initial = {})
|
|
9
11
|
@values = initial.each_with_object({}) do |(key, value), output|
|
|
@@ -53,6 +55,42 @@ module Vizcore
|
|
|
53
55
|
@values[:deadzone] = value
|
|
54
56
|
end
|
|
55
57
|
|
|
58
|
+
# @param value [Numeric]
|
|
59
|
+
# @return [Numeric]
|
|
60
|
+
def threshold(value)
|
|
61
|
+
@values[:threshold] = value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param value [Numeric]
|
|
65
|
+
# @return [Numeric]
|
|
66
|
+
def hysteresis(value)
|
|
67
|
+
@values[:hysteresis] = value
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param value [Numeric] hold duration in seconds at the runtime frame cadence
|
|
71
|
+
# @return [Numeric]
|
|
72
|
+
def hold(value)
|
|
73
|
+
@values[:hold] = value
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param value [Numeric] per-frame decay multiplier
|
|
77
|
+
# @return [Numeric]
|
|
78
|
+
def decay(value)
|
|
79
|
+
@values[:decay] = value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param seconds [Numeric]
|
|
83
|
+
# @return [Numeric]
|
|
84
|
+
def cooldown(seconds)
|
|
85
|
+
@values[:cooldown] = seconds
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param enabled [Boolean, nil]
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def one_shot(enabled = true)
|
|
91
|
+
@values[:one_shot] = !!enabled
|
|
92
|
+
end
|
|
93
|
+
|
|
56
94
|
# @param attack [Numeric, nil]
|
|
57
95
|
# @param release [Numeric, nil]
|
|
58
96
|
# @return [Hash]
|
|
@@ -62,6 +100,18 @@ module Vizcore
|
|
|
62
100
|
@values
|
|
63
101
|
end
|
|
64
102
|
|
|
103
|
+
# @param mode [Symbol, String]
|
|
104
|
+
# @return [Symbol]
|
|
105
|
+
def as(mode)
|
|
106
|
+
normalized = mode.to_sym
|
|
107
|
+
unless TRIGGER_MODES.include?(normalized)
|
|
108
|
+
raise ArgumentError, "mapping as must be :continuous or :trigger"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@values[:as] = normalized
|
|
112
|
+
normalized
|
|
113
|
+
end
|
|
114
|
+
|
|
65
115
|
# @return [Hash]
|
|
66
116
|
def to_h
|
|
67
117
|
@values.dup
|
|
@@ -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")
|