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
|
@@ -2,30 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "mapping_transform_builder"
|
|
4
4
|
require_relative "reaction_builder"
|
|
5
|
+
require_relative "../deep_copy"
|
|
6
|
+
require_relative "../layer_catalog"
|
|
7
|
+
require_relative "../shape"
|
|
8
|
+
require_relative "color_helpers"
|
|
9
|
+
require_relative "layout_helpers"
|
|
5
10
|
|
|
6
11
|
module Vizcore
|
|
7
12
|
module DSL
|
|
8
13
|
# Builder for one render layer in a scene.
|
|
9
14
|
class LayerBuilder
|
|
10
15
|
NO_ARGUMENT = Object.new.freeze
|
|
16
|
+
SHAPE_SCHEMA_VERSION = 2
|
|
11
17
|
MAPPING_SOURCE_KINDS = %i[
|
|
12
|
-
amplitude frequency_band fft_spectrum onset kick snare hihat beat beat_confidence beat_pulse beat_count bpm
|
|
18
|
+
amplitude peak frequency_band frequency_band_peak fft_spectrum onset kick snare hihat beat beat_confidence beat_pulse beat_count bpm
|
|
19
|
+
beat_phase beat_2 beat_4 beat_8 beat_triplet triplet bar_phase bar_count phrase_count bpm_confidence
|
|
20
|
+
spectral_centroid spectral_rolloff spectral_flatness spectral_flux zero_crossing_rate global lfo adsr envelope
|
|
13
21
|
].freeze
|
|
22
|
+
PATH_DEFAULT_DETAIL = 32
|
|
23
|
+
PATH_MIN_DETAIL = 4
|
|
24
|
+
PATH_MAX_DETAIL = 128
|
|
25
|
+
PATH_DEFAULT_MAX_SEGMENTS = 4096
|
|
26
|
+
SHAPE_TARGET_ALIASES = {
|
|
27
|
+
"translate_x" => "transform.translate.x",
|
|
28
|
+
"translate_y" => "transform.translate.y",
|
|
29
|
+
"rotate" => "transform.rotate",
|
|
30
|
+
"rotation" => "transform.rotate",
|
|
31
|
+
"scale" => "transform.scale",
|
|
32
|
+
"scale_x" => "transform.scale.x",
|
|
33
|
+
"scale_y" => "transform.scale.y",
|
|
34
|
+
"origin_x" => "transform.origin.x",
|
|
35
|
+
"origin_y" => "transform.origin.y"
|
|
36
|
+
}.freeze
|
|
37
|
+
SHAPE_STYLE_KEYS = Vizcore::Shape::STYLE_KEYS
|
|
38
|
+
SHAPE_TRANSFORM_KEYS = %i[translate rotate rotation scale origin].freeze
|
|
39
|
+
STRICT_PARAM_ALLOWLIST = %i[
|
|
40
|
+
custom_shape_controls custom_shapes glsl_source group origin rotate scale translate
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# Reference to an already declared shape, used by `map ... to: shape(:id).radius`.
|
|
44
|
+
class ShapeReference
|
|
45
|
+
def initialize(prefix)
|
|
46
|
+
@prefix = prefix
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def method_missing(method_name, *args, &block)
|
|
50
|
+
return super unless args.empty? && block.nil?
|
|
51
|
+
|
|
52
|
+
target = SHAPE_TARGET_ALIASES.fetch(method_name.to_s, method_name.to_s)
|
|
53
|
+
:"#{@prefix}.#{target}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
14
60
|
|
|
15
61
|
# @param name [Symbol, String] layer identifier
|
|
16
62
|
# @param styles [Hash] reusable layer parameter styles
|
|
17
63
|
# @param defaults [Hash] default params applied before layer-specific values
|
|
18
|
-
|
|
64
|
+
# @param strict [Boolean] true when unknown layer params should fail
|
|
65
|
+
# @param mapping_presets [Hash] reusable mapping presets
|
|
66
|
+
def initialize(name:, styles: {}, defaults: {}, mapping_presets: {}, strict: false)
|
|
19
67
|
@name = name.to_sym
|
|
20
68
|
@styles = styles
|
|
69
|
+
@mapping_presets = mapping_presets
|
|
70
|
+
@strict = !!strict
|
|
21
71
|
@type = nil
|
|
22
72
|
@shader = nil
|
|
23
73
|
@glsl = nil
|
|
24
74
|
@params = deep_dup(defaults)
|
|
25
75
|
@param_schema = {}
|
|
26
76
|
@mappings = []
|
|
77
|
+
@shape_index_by_id = {}
|
|
78
|
+
@shape_group_stack = [{}]
|
|
27
79
|
end
|
|
28
80
|
|
|
81
|
+
include ColorHelpers
|
|
82
|
+
include LayoutHelpers
|
|
83
|
+
|
|
29
84
|
# Evaluate a layer block.
|
|
30
85
|
#
|
|
31
86
|
# @yield Layer DSL methods
|
|
@@ -72,8 +127,8 @@ module Vizcore
|
|
|
72
127
|
# @param options [Hash] shape params such as `count`, `radius`, `x`, and `y`
|
|
73
128
|
# @yield optional block evaluated in the shape context
|
|
74
129
|
# @return [Hash]
|
|
75
|
-
def circle(**options, &block)
|
|
76
|
-
build_shape(:circle, options, &block)
|
|
130
|
+
def circle(id = nil, **options, &block)
|
|
131
|
+
build_shape(:circle, shape_options(id, options), &block)
|
|
77
132
|
end
|
|
78
133
|
|
|
79
134
|
# Declare a 2D line primitive for a shape layer.
|
|
@@ -81,8 +136,133 @@ module Vizcore
|
|
|
81
136
|
# @param options [Hash] shape params such as `x1`, `y1`, `x2`, and `y2`
|
|
82
137
|
# @yield optional block evaluated in the shape context
|
|
83
138
|
# @return [Hash]
|
|
84
|
-
def line(**options, &block)
|
|
85
|
-
build_shape(:line, options, &block)
|
|
139
|
+
def line(id = nil, **options, &block)
|
|
140
|
+
build_shape(:line, shape_options(id, options), &block)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Declare a 2D rectangle primitive for a shape layer.
|
|
144
|
+
#
|
|
145
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
146
|
+
# @param options [Hash] shape params such as `x`, `y`, `width`, `height`, and `radius`
|
|
147
|
+
# @yield optional block evaluated in the shape context
|
|
148
|
+
# @return [Hash]
|
|
149
|
+
def rect(id = nil, **options, &block)
|
|
150
|
+
build_shape(:rect, shape_options(id, options), schema_version: true, &block)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Declare a closed polygon primitive for a shape layer.
|
|
154
|
+
#
|
|
155
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
156
|
+
# @param options [Hash] shape params including `points`
|
|
157
|
+
# @yield optional block evaluated in the shape context
|
|
158
|
+
# @return [Hash]
|
|
159
|
+
def polygon(id = nil, **options, &block)
|
|
160
|
+
build_shape(:polygon, shape_options(id, options), schema_version: true, &block)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Declare an open polyline primitive for a shape layer.
|
|
164
|
+
#
|
|
165
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
166
|
+
# @param options [Hash] shape params including `points`
|
|
167
|
+
# @yield optional block evaluated in the shape context
|
|
168
|
+
# @return [Hash]
|
|
169
|
+
def polyline(id = nil, **options, &block)
|
|
170
|
+
build_shape(:polyline, shape_options(id, options).merge(closed: false), schema_version: true, &block)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Declare a path primitive using SVG-like path commands.
|
|
174
|
+
#
|
|
175
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
176
|
+
# @param options [Hash] path params such as `detail`
|
|
177
|
+
# @yield block containing path commands and shape styling
|
|
178
|
+
# @return [Hash]
|
|
179
|
+
def path(id = nil, **options, &block)
|
|
180
|
+
shape = shape_options(id, options)
|
|
181
|
+
shape[:commands] ||= []
|
|
182
|
+
build_shape(:path, shape, schema_version: true, &block)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Declare a quadratic or cubic bezier curve. The serialized primitive is a path.
|
|
186
|
+
#
|
|
187
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
188
|
+
# @param from [Array<Numeric>] start point
|
|
189
|
+
# @param to [Array<Numeric>] end point
|
|
190
|
+
# @param control [Array<Numeric>, nil] quadratic control point
|
|
191
|
+
# @param c1 [Array<Numeric>, nil] first cubic control point
|
|
192
|
+
# @param c2 [Array<Numeric>, nil] second cubic control point
|
|
193
|
+
# @param options [Hash] additional path params
|
|
194
|
+
# @yield optional block evaluated in the shape context
|
|
195
|
+
# @return [Hash]
|
|
196
|
+
def bezier(id = nil, from:, to:, control: nil, c1: nil, c2: nil, **options, &block)
|
|
197
|
+
commands = [["M", *point_values(from)]]
|
|
198
|
+
if control
|
|
199
|
+
commands << ["Q", *point_values(control), *point_values(to)]
|
|
200
|
+
elsif c1 && c2
|
|
201
|
+
commands << ["C", *point_values(c1), *point_values(c2), *point_values(to)]
|
|
202
|
+
else
|
|
203
|
+
raise ArgumentError, "bezier requires either :control or both :c1 and :c2"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
build_shape(:path, shape_options(id, options).merge(commands: commands), schema_version: true, &block)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Declare a star polygon primitive for a shape layer.
|
|
210
|
+
#
|
|
211
|
+
# @param id [Symbol, String, nil] optional shape identifier
|
|
212
|
+
# @param options [Hash] shape params such as `points`, `radius`, and `inner_radius`
|
|
213
|
+
# @yield optional block evaluated in the shape context
|
|
214
|
+
# @return [Hash]
|
|
215
|
+
def star(id = nil, **options, &block)
|
|
216
|
+
build_shape(:star, shape_options(id, options), schema_version: true, &block)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Expand a registered Ruby custom shape into normal shape primitives.
|
|
220
|
+
#
|
|
221
|
+
# @param renderer [Symbol, String, Class, Module, #call] registered shape name or renderer
|
|
222
|
+
# @param options [Hash] custom shape params
|
|
223
|
+
# @yield optional block applied to each generated primitive
|
|
224
|
+
# @return [Array<Hash>]
|
|
225
|
+
def custom_shape(renderer, **options, &block)
|
|
226
|
+
mark_shape_schema_version!
|
|
227
|
+
shape_id = options.delete(:id)
|
|
228
|
+
dynamic = options.delete(:dynamic)
|
|
229
|
+
static = options.delete(:static)
|
|
230
|
+
raise ArgumentError, "custom_shape cannot be both static and dynamic" if dynamic && static
|
|
231
|
+
|
|
232
|
+
dynamic = true if static == false
|
|
233
|
+
return append_dynamic_custom_shape(renderer, options, shape_id: shape_id, &block) if dynamic
|
|
234
|
+
|
|
235
|
+
primitives = expand_custom_shape(renderer, options, shape_id: shape_id, cache: !!static)
|
|
236
|
+
raise ArgumentError, "custom_shape produced no primitives" if primitives.empty?
|
|
237
|
+
raise ArgumentError, "custom_shape id can only be assigned when one primitive is produced" if shape_id && primitives.length > 1
|
|
238
|
+
|
|
239
|
+
@type ||= :shape
|
|
240
|
+
@params[:shapes] ||= []
|
|
241
|
+
primitives.map do |primitive|
|
|
242
|
+
primitive[:id] ||= shape_id.to_sym if shape_id
|
|
243
|
+
append_expanded_shape(primitive, &block)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Apply shared style and transform to shape primitives declared in the block.
|
|
248
|
+
#
|
|
249
|
+
# Group attributes are flattened into child primitives so the frontend only
|
|
250
|
+
# needs to render regular shape primitives.
|
|
251
|
+
#
|
|
252
|
+
# @param id [Symbol, String, nil] optional group identifier, currently documentation-only
|
|
253
|
+
# @param attrs [Hash] initial group style/transform attrs
|
|
254
|
+
# @yield shape declarations
|
|
255
|
+
# @return [Array<Hash>]
|
|
256
|
+
def group(_id = nil, **attrs, &block)
|
|
257
|
+
raise ArgumentError, "group requires a block" unless block
|
|
258
|
+
|
|
259
|
+
mark_shape_schema_version!
|
|
260
|
+
@type ||= :shape
|
|
261
|
+
@shape_group_stack << merge_shape_group(current_shape_group, normalize_shape_group(attrs))
|
|
262
|
+
instance_eval(&block)
|
|
263
|
+
@params[:shapes] || []
|
|
264
|
+
ensure
|
|
265
|
+
@shape_group_stack.pop if @shape_group_stack.length > 1
|
|
86
266
|
end
|
|
87
267
|
|
|
88
268
|
# Group shape primitives in a block for readability.
|
|
@@ -146,6 +326,22 @@ module Vizcore
|
|
|
146
326
|
# @param value [String] text fill color
|
|
147
327
|
# @return [String]
|
|
148
328
|
def fill(value)
|
|
329
|
+
if @current_shape
|
|
330
|
+
@current_shape[:fill] = value.to_s
|
|
331
|
+
mark_shape_schema_version!
|
|
332
|
+
return @current_shape
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
if @current_custom_shape
|
|
336
|
+
current_custom_shape_style[:fill] = value.to_s
|
|
337
|
+
return @current_custom_shape
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
if in_shape_group?
|
|
341
|
+
current_shape_group[:fill] = value.to_s
|
|
342
|
+
return current_shape_group
|
|
343
|
+
end
|
|
344
|
+
|
|
149
345
|
@params[:color] = value.to_s
|
|
150
346
|
end
|
|
151
347
|
|
|
@@ -160,6 +356,20 @@ module Vizcore
|
|
|
160
356
|
return @current_shape
|
|
161
357
|
end
|
|
162
358
|
|
|
359
|
+
if @current_custom_shape
|
|
360
|
+
current_custom_shape_style[:stroke] = normalize_non_negative_param_number(value, :stroke) unless value.equal?(NO_ARGUMENT)
|
|
361
|
+
current_custom_shape_style[:stroke_width] = normalize_non_negative_param_number(width, :stroke_width) unless width.nil?
|
|
362
|
+
current_custom_shape_style[:stroke_color] = color.to_s unless color.nil?
|
|
363
|
+
return @current_custom_shape
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
if in_shape_group?
|
|
367
|
+
current_shape_group[:stroke] = normalize_non_negative_param_number(value, :stroke) unless value.equal?(NO_ARGUMENT)
|
|
368
|
+
current_shape_group[:stroke_width] = normalize_non_negative_param_number(width, :stroke_width) unless width.nil?
|
|
369
|
+
current_shape_group[:stroke_color] = color.to_s unless color.nil?
|
|
370
|
+
return current_shape_group
|
|
371
|
+
end
|
|
372
|
+
|
|
163
373
|
@params[:stroke_width] = normalize_non_negative_param_number(width, :stroke_width) unless width.nil?
|
|
164
374
|
@params[:stroke_color] = color.to_s unless color.nil?
|
|
165
375
|
@params
|
|
@@ -177,9 +387,198 @@ module Vizcore
|
|
|
177
387
|
# @param value [Symbol, String] layer compositing mode
|
|
178
388
|
# @return [Symbol]
|
|
179
389
|
def blend(value)
|
|
390
|
+
if @current_shape
|
|
391
|
+
@current_shape[:blend] = value.to_sym
|
|
392
|
+
mark_shape_schema_version!
|
|
393
|
+
return @current_shape
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
if @current_custom_shape
|
|
397
|
+
current_custom_shape_style[:blend] = value.to_sym
|
|
398
|
+
return @current_custom_shape
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
if in_shape_group?
|
|
402
|
+
current_shape_group[:blend] = value.to_sym
|
|
403
|
+
return current_shape_group
|
|
404
|
+
end
|
|
405
|
+
|
|
180
406
|
@params[:blend] = value.to_sym
|
|
181
407
|
end
|
|
182
408
|
|
|
409
|
+
# Set layer or shape opacity.
|
|
410
|
+
#
|
|
411
|
+
# @param value [Numeric]
|
|
412
|
+
# @return [Float, Hash]
|
|
413
|
+
def opacity(value)
|
|
414
|
+
if @current_shape
|
|
415
|
+
@current_shape[:opacity] = normalize_param_number(value, :opacity)
|
|
416
|
+
mark_shape_schema_version!
|
|
417
|
+
return @current_shape
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
if @current_custom_shape
|
|
421
|
+
current_custom_shape_style[:opacity] = normalize_param_number(value, :opacity)
|
|
422
|
+
return @current_custom_shape
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
if in_shape_group?
|
|
426
|
+
current_shape_group[:opacity] = current_shape_group.key?(:opacity) ? normalize_param_number(current_shape_group[:opacity], :opacity) * normalize_param_number(value, :opacity) : normalize_param_number(value, :opacity)
|
|
427
|
+
return current_shape_group
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
@params[:opacity] = normalize_param_number(value, :opacity)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Set a shape/layer translation transform.
|
|
434
|
+
#
|
|
435
|
+
# @param args [Array<Numeric>]
|
|
436
|
+
# @param x [Numeric, nil]
|
|
437
|
+
# @param y [Numeric, nil]
|
|
438
|
+
# @return [Hash]
|
|
439
|
+
def translate(*args, x: nil, y: nil)
|
|
440
|
+
values = normalize_xy_args(args, x: x, y: y, name: :translate)
|
|
441
|
+
if @current_shape
|
|
442
|
+
current_shape_transform[:translate] = values
|
|
443
|
+
return @current_shape
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
if @current_custom_shape
|
|
447
|
+
current_custom_shape_transform[:translate] = add_shape_xy(current_custom_shape_transform[:translate], values)
|
|
448
|
+
return @current_custom_shape
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
if in_shape_group?
|
|
452
|
+
current_shape_group_transform[:translate] = add_shape_xy(current_shape_group_transform[:translate], values)
|
|
453
|
+
return current_shape_group
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
@params[:translate] = values
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Set a shape/layer rotation transform in degrees.
|
|
460
|
+
#
|
|
461
|
+
# @param value [Numeric]
|
|
462
|
+
# @return [Float, Hash]
|
|
463
|
+
def rotate(value)
|
|
464
|
+
rotation = normalize_param_number(value, :rotate)
|
|
465
|
+
if @current_shape
|
|
466
|
+
current_shape_transform[:rotate] = rotation
|
|
467
|
+
return @current_shape
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
if @current_custom_shape
|
|
471
|
+
current_custom_shape_transform[:rotate] = normalize_param_number(current_custom_shape_transform[:rotate] || 0, :rotate) + rotation
|
|
472
|
+
return @current_custom_shape
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
if in_shape_group?
|
|
476
|
+
current_shape_group_transform[:rotate] = normalize_param_number(current_shape_group_transform[:rotate] || 0, :rotate) + rotation
|
|
477
|
+
return current_shape_group
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
@params[:rotate] = rotation
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Set a shape/layer scale transform.
|
|
484
|
+
#
|
|
485
|
+
# @param value [Numeric]
|
|
486
|
+
# @param x [Numeric, nil]
|
|
487
|
+
# @param y [Numeric, nil]
|
|
488
|
+
# @return [Float, Hash]
|
|
489
|
+
def scale(value = NO_ARGUMENT, x: nil, y: nil)
|
|
490
|
+
scale_value = normalize_scale_args(value, x: x, y: y)
|
|
491
|
+
if @current_shape
|
|
492
|
+
current_shape_transform[:scale] = scale_value
|
|
493
|
+
return @current_shape
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
if @current_custom_shape
|
|
497
|
+
current_custom_shape_transform[:scale] = multiply_shape_scale(current_custom_shape_transform[:scale], scale_value)
|
|
498
|
+
return @current_custom_shape
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
if in_shape_group?
|
|
502
|
+
current_shape_group_transform[:scale] = multiply_shape_scale(current_shape_group_transform[:scale], scale_value)
|
|
503
|
+
return current_shape_group
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
@params[:scale] = scale_value
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Set a shape/layer transform origin.
|
|
510
|
+
#
|
|
511
|
+
# @param args [Array<Numeric>]
|
|
512
|
+
# @param x [Numeric, nil]
|
|
513
|
+
# @param y [Numeric, nil]
|
|
514
|
+
# @return [Hash]
|
|
515
|
+
def origin(*args, x: nil, y: nil)
|
|
516
|
+
values = normalize_xy_args(args, x: x, y: y, name: :origin)
|
|
517
|
+
if @current_shape
|
|
518
|
+
current_shape_transform[:origin] = values
|
|
519
|
+
return @current_shape
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
if @current_custom_shape
|
|
523
|
+
current_custom_shape_transform[:origin] = values
|
|
524
|
+
return @current_custom_shape
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
if in_shape_group?
|
|
528
|
+
current_shape_group_transform[:origin] = values
|
|
529
|
+
return current_shape_group
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
@params[:origin] = values
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Return a reference object for mapping to a named shape.
|
|
536
|
+
#
|
|
537
|
+
# @param id [Symbol, String]
|
|
538
|
+
# @return [ShapeReference]
|
|
539
|
+
def shape(id)
|
|
540
|
+
key = id.to_sym
|
|
541
|
+
index = @shape_index_by_id.fetch(key) do
|
|
542
|
+
suggestion_message = shape_id_suggestions(key)
|
|
543
|
+
message = "unknown shape id: #{key.inspect}"
|
|
544
|
+
message = "#{message}. Did you mean: #{suggestion_message}" unless suggestion_message.empty?
|
|
545
|
+
raise ArgumentError, message
|
|
546
|
+
end
|
|
547
|
+
ShapeReference.new("shapes.#{index}")
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def move_to(x, y)
|
|
551
|
+
append_path_command("M", x, y)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def line_to(x, y)
|
|
555
|
+
append_path_command("L", x, y)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def quad_to(cx, cy, x, y)
|
|
559
|
+
append_path_command("Q", cx, cy, x, y)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def cubic_to(c1x, c1y, c2x, c2y, x, y)
|
|
563
|
+
append_path_command("C", c1x, c1y, c2x, c2y, x, y)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def horizontal_to(x)
|
|
567
|
+
append_path_command("H", x)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def vertical_to(y)
|
|
571
|
+
append_path_command("V", y)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def arc_to(rx, ry, rotation, large_arc, sweep, x, y)
|
|
575
|
+
append_path_command("A", rx, ry, rotation, large_arc, sweep, x, y)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def close
|
|
579
|
+
append_path_command("Z")
|
|
580
|
+
end
|
|
581
|
+
|
|
183
582
|
# Store an ordered color palette for this layer.
|
|
184
583
|
#
|
|
185
584
|
# @param colors [Array<String, Array<String>>] color values such as "#00ffff"
|
|
@@ -189,6 +588,17 @@ module Vizcore
|
|
|
189
588
|
@params[:palette] = normalize_palette(colors)
|
|
190
589
|
end
|
|
191
590
|
|
|
591
|
+
# Append one post effect to this layer's post effect chain.
|
|
592
|
+
#
|
|
593
|
+
# @param name [Symbol, String] effect key
|
|
594
|
+
# @return [Symbol]
|
|
595
|
+
def post(name)
|
|
596
|
+
raise ArgumentError, "post expects a symbol or string" unless name
|
|
597
|
+
|
|
598
|
+
@params[:post_effects] ||= []
|
|
599
|
+
@params[:post_effects] << name.to_sym
|
|
600
|
+
end
|
|
601
|
+
|
|
192
602
|
# Apply a named style by merging its params into this layer.
|
|
193
603
|
#
|
|
194
604
|
# @param name [Symbol, String] style identifier
|
|
@@ -232,6 +642,7 @@ module Vizcore
|
|
|
232
642
|
# @raise [ArgumentError] when the mapping is empty or invalid
|
|
233
643
|
# @return [void]
|
|
234
644
|
def map(definition = nil, **options, &block)
|
|
645
|
+
definition, options = normalize_custom_shape_mapping(definition, options) if @custom_shape_target_prefix
|
|
235
646
|
definition, options = normalize_shape_mapping(definition, options) if @shape_target_prefix
|
|
236
647
|
|
|
237
648
|
if options.key?(:to)
|
|
@@ -257,6 +668,17 @@ module Vizcore
|
|
|
257
668
|
end
|
|
258
669
|
end
|
|
259
670
|
|
|
671
|
+
# Apply a named mapping preset to this layer.
|
|
672
|
+
#
|
|
673
|
+
# @param name [Symbol, String] mapping preset identifier
|
|
674
|
+
# @raise [ArgumentError] when the preset is unknown
|
|
675
|
+
# @return [void]
|
|
676
|
+
def use_mapping(name)
|
|
677
|
+
preset_name = name.to_sym
|
|
678
|
+
preset = @mapping_presets.fetch(preset_name) { raise ArgumentError, "unknown mapping preset: #{preset_name}" }
|
|
679
|
+
preset.each { |mapping| @mappings << deep_dup(mapping) }
|
|
680
|
+
end
|
|
681
|
+
|
|
260
682
|
# High-level mapping DSL for describing audio reactions inside a layer.
|
|
261
683
|
#
|
|
262
684
|
# @param source_value [Hash, Symbol, String] analysis source descriptor
|
|
@@ -284,42 +706,82 @@ module Vizcore
|
|
|
284
706
|
mapping_source(:amplitude)
|
|
285
707
|
end
|
|
286
708
|
|
|
709
|
+
# @return [Hash] source descriptor for absolute sample peak level
|
|
710
|
+
def peak
|
|
711
|
+
mapping_source(:peak)
|
|
712
|
+
end
|
|
713
|
+
|
|
287
714
|
# @param name [Symbol, String] band key (`sub`, `low`, `mid`, `high`)
|
|
288
715
|
# @return [Hash] source descriptor for a frequency band
|
|
289
716
|
def frequency_band(name)
|
|
290
717
|
mapping_source(:frequency_band, band: name.to_sym)
|
|
291
718
|
end
|
|
292
719
|
|
|
720
|
+
# @return [Hash] source descriptor for a held frequency-band peak
|
|
721
|
+
def frequency_band_peak(name)
|
|
722
|
+
mapping_source(:frequency_band_peak, band: name.to_sym)
|
|
723
|
+
end
|
|
724
|
+
|
|
293
725
|
# @return [Hash] source descriptor for the sub-bass frequency band
|
|
294
726
|
def sub
|
|
295
727
|
frequency_band(:sub)
|
|
296
728
|
end
|
|
297
729
|
|
|
730
|
+
# @return [Hash] source descriptor for the held sub-bass peak
|
|
731
|
+
def sub_peak
|
|
732
|
+
frequency_band_peak(:sub)
|
|
733
|
+
end
|
|
734
|
+
|
|
298
735
|
# @return [Hash] source descriptor for the low/bass frequency band
|
|
299
736
|
def low
|
|
300
737
|
frequency_band(:low)
|
|
301
738
|
end
|
|
302
739
|
|
|
740
|
+
# @return [Hash] source descriptor for the held low/bass peak
|
|
741
|
+
def low_peak
|
|
742
|
+
frequency_band_peak(:low)
|
|
743
|
+
end
|
|
744
|
+
|
|
303
745
|
# @return [Hash] source descriptor for the low/bass frequency band
|
|
304
746
|
def bass
|
|
305
747
|
frequency_band(:low)
|
|
306
748
|
end
|
|
307
749
|
|
|
750
|
+
# @return [Hash] source descriptor for the held low/bass peak
|
|
751
|
+
def bass_peak
|
|
752
|
+
frequency_band_peak(:low)
|
|
753
|
+
end
|
|
754
|
+
|
|
308
755
|
# @return [Hash] source descriptor for the mid frequency band
|
|
309
756
|
def mid
|
|
310
757
|
frequency_band(:mid)
|
|
311
758
|
end
|
|
312
759
|
|
|
760
|
+
# @return [Hash] source descriptor for the held mid peak
|
|
761
|
+
def mid_peak
|
|
762
|
+
frequency_band_peak(:mid)
|
|
763
|
+
end
|
|
764
|
+
|
|
313
765
|
# @return [Hash] source descriptor for the high frequency band
|
|
314
766
|
def high
|
|
315
767
|
frequency_band(:high)
|
|
316
768
|
end
|
|
317
769
|
|
|
770
|
+
# @return [Hash] source descriptor for the held high peak
|
|
771
|
+
def high_peak
|
|
772
|
+
frequency_band_peak(:high)
|
|
773
|
+
end
|
|
774
|
+
|
|
318
775
|
# @return [Hash] source descriptor for the high/treble frequency band
|
|
319
776
|
def treble
|
|
320
777
|
frequency_band(:high)
|
|
321
778
|
end
|
|
322
779
|
|
|
780
|
+
# @return [Hash] source descriptor for the held high/treble peak
|
|
781
|
+
def treble_peak
|
|
782
|
+
frequency_band_peak(:high)
|
|
783
|
+
end
|
|
784
|
+
|
|
323
785
|
# @return [Hash] source descriptor for FFT spectrum array
|
|
324
786
|
def fft_spectrum
|
|
325
787
|
mapping_source(:fft_spectrum)
|
|
@@ -378,13 +840,138 @@ module Vizcore
|
|
|
378
840
|
mapping_source(:beat_count)
|
|
379
841
|
end
|
|
380
842
|
|
|
843
|
+
# @return [Hash] source descriptor for 0.0..1.0 phase within the current beat
|
|
844
|
+
def beat_phase
|
|
845
|
+
mapping_source(:beat_phase)
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# @return [Hash] source descriptor for half-beat subdivision pulses
|
|
849
|
+
def beat_2
|
|
850
|
+
mapping_source(:beat_2)
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
# @return [Hash] source descriptor for quarter-beat subdivision pulses
|
|
854
|
+
def beat_4
|
|
855
|
+
mapping_source(:beat_4)
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# @return [Hash] source descriptor for eighth-beat subdivision pulses
|
|
859
|
+
def beat_8
|
|
860
|
+
mapping_source(:beat_8)
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
# @return [Hash] source descriptor for triplet subdivision pulses
|
|
864
|
+
def beat_triplet
|
|
865
|
+
mapping_source(:beat_triplet)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
# @return [Hash] source descriptor for triplet subdivision pulses
|
|
869
|
+
def triplet
|
|
870
|
+
mapping_source(:beat_triplet)
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# @return [Hash] source descriptor for 0.0..1.0 phase within the current 4-beat bar
|
|
874
|
+
def bar_phase
|
|
875
|
+
mapping_source(:bar_phase)
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
# @return [Hash] source descriptor for completed 4-beat bars
|
|
879
|
+
def bar_count(value = NO_ARGUMENT)
|
|
880
|
+
return @params[:bar_count] = Integer(value) unless value.equal?(NO_ARGUMENT)
|
|
881
|
+
|
|
882
|
+
mapping_source(:bar_count)
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# @return [Hash] source descriptor for completed 8-bar phrases
|
|
886
|
+
def phrase_count
|
|
887
|
+
mapping_source(:phrase_count)
|
|
888
|
+
end
|
|
889
|
+
|
|
381
890
|
# @return [Hash] source descriptor for estimated BPM
|
|
382
891
|
def bpm
|
|
383
892
|
mapping_source(:bpm)
|
|
384
893
|
end
|
|
385
894
|
|
|
895
|
+
# @return [Hash] source descriptor for tempo estimator confidence
|
|
896
|
+
def bpm_confidence
|
|
897
|
+
mapping_source(:bpm_confidence)
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# @return [Hash] source descriptor for spectral centroid in Hz
|
|
901
|
+
def spectral_centroid
|
|
902
|
+
mapping_source(:spectral_centroid)
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
# @return [Hash] source descriptor for spectral rolloff in Hz
|
|
906
|
+
def spectral_rolloff
|
|
907
|
+
mapping_source(:spectral_rolloff)
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
# @return [Hash] source descriptor for spectral flatness
|
|
911
|
+
def spectral_flatness
|
|
912
|
+
mapping_source(:spectral_flatness)
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# @return [Hash] source descriptor for positive spectrum delta
|
|
916
|
+
def spectral_flux
|
|
917
|
+
mapping_source(:spectral_flux)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
# @return [Hash] source descriptor for time-domain zero crossing rate
|
|
921
|
+
def zero_crossing_rate
|
|
922
|
+
mapping_source(:zero_crossing_rate)
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# @param name [Symbol, String] runtime global value name
|
|
926
|
+
# @return [Hash] source descriptor for mutable runtime globals
|
|
927
|
+
def global(name)
|
|
928
|
+
mapping_source(:global, name: name.to_sym)
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
# @param wave [Symbol, String] one of `sine`, `triangle`, `saw`, `square`
|
|
932
|
+
# @param rate [Numeric] cycles per second
|
|
933
|
+
# @param phase [Numeric] phase offset in cycles
|
|
934
|
+
# @return [Hash] source descriptor for a time-based low-frequency oscillator
|
|
935
|
+
def lfo(wave = :sine, rate: 1.0, phase: 0.0)
|
|
936
|
+
mapping_source(:lfo, wave: wave.to_sym, rate: Float(rate), phase: Float(phase))
|
|
937
|
+
rescue ArgumentError, TypeError
|
|
938
|
+
raise ArgumentError, "lfo rate and phase must be numeric"
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
# @param source [Symbol, Hash] source to trigger the envelope
|
|
942
|
+
# @param attack [Numeric] seconds from 0.0 to peak
|
|
943
|
+
# @param decay [Numeric] seconds from peak to sustain
|
|
944
|
+
# @param sustain [Numeric] sustain gain once decay is complete (0.0..1.0)
|
|
945
|
+
# @param release [Numeric] seconds from sustain to 0.0
|
|
946
|
+
# @param threshold [Numeric] value threshold that starts attack
|
|
947
|
+
# @param peak [Numeric] peak gain multiplier
|
|
948
|
+
# @return [Hash] source descriptor for an ADSR envelope
|
|
949
|
+
def adsr(source = :kick, attack: 0.02, decay: 0.08, sustain: 0.7, release: 0.16, threshold: 0.0, peak: 1.0)
|
|
950
|
+
source_value = source.nil? ? { kind: :kick } : normalize_source(source)
|
|
951
|
+
mapping_source(
|
|
952
|
+
:adsr,
|
|
953
|
+
source: source_value,
|
|
954
|
+
attack: normalize_non_negative_float(attack, :attack),
|
|
955
|
+
decay: normalize_non_negative_float(decay, :decay),
|
|
956
|
+
sustain: clamp(normalize_float(sustain, :sustain), 0.0, 1.0),
|
|
957
|
+
release: normalize_non_negative_float(release, :release),
|
|
958
|
+
threshold: normalize_float(threshold, :threshold),
|
|
959
|
+
peak: normalize_float(peak, :peak)
|
|
960
|
+
)
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
# Alias for ADSR envelope mapping source.
|
|
964
|
+
#
|
|
965
|
+
# @param source [Symbol, Hash] source to trigger the envelope
|
|
966
|
+
# @param options [Numeric] envelope timing and shaping values
|
|
967
|
+
# @return [Hash] source descriptor for a general envelope
|
|
968
|
+
def envelope(source = :kick, **options)
|
|
969
|
+
adsr(source, **options)
|
|
970
|
+
end
|
|
971
|
+
|
|
386
972
|
# @return [Hash] serialized layer payload
|
|
387
973
|
def to_h
|
|
974
|
+
validate_strict_params! if @strict
|
|
388
975
|
layer = {
|
|
389
976
|
name: @name,
|
|
390
977
|
type: resolved_type,
|
|
@@ -405,6 +992,16 @@ module Vizcore
|
|
|
405
992
|
return args.first
|
|
406
993
|
end
|
|
407
994
|
|
|
995
|
+
if @current_custom_shape && block.nil? && args.length == 1
|
|
996
|
+
@current_custom_shape[:params][method_name.to_sym] = args.first
|
|
997
|
+
return args.first
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
if in_shape_group? && block.nil? && args.length == 1
|
|
1001
|
+
current_shape_group[method_name.to_sym] = args.first
|
|
1002
|
+
return args.first
|
|
1003
|
+
end
|
|
1004
|
+
|
|
408
1005
|
if block.nil? && args.length == 1
|
|
409
1006
|
@params[method_name.to_sym] = args.first
|
|
410
1007
|
return args.first
|
|
@@ -414,25 +1011,77 @@ module Vizcore
|
|
|
414
1011
|
end
|
|
415
1012
|
|
|
416
1013
|
def respond_to_missing?(method_name, include_private = false)
|
|
417
|
-
@params.key?(method_name.to_sym) || super
|
|
1014
|
+
!!@current_custom_shape || @params.key?(method_name.to_sym) || super
|
|
418
1015
|
end
|
|
419
1016
|
|
|
420
1017
|
private
|
|
421
1018
|
|
|
422
|
-
def build_shape(kind, options, &block)
|
|
1019
|
+
def build_shape(kind, options, schema_version: false, &block)
|
|
423
1020
|
@type ||= :shape
|
|
1021
|
+
mark_shape_schema_version! if schema_version
|
|
424
1022
|
shape = normalize_shape(kind, options)
|
|
425
1023
|
@params[:shapes] ||= []
|
|
426
1024
|
shape_index = @params[:shapes].length
|
|
1025
|
+
register_shape_id!(shape, shape_index)
|
|
427
1026
|
@params[:shapes] << shape
|
|
428
1027
|
|
|
429
1028
|
with_shape_context(shape, shape_index) do
|
|
430
1029
|
instance_eval(&block) if block
|
|
431
1030
|
end
|
|
1031
|
+
apply_current_shape_group!(shape)
|
|
1032
|
+
validate_shape!(shape)
|
|
432
1033
|
|
|
433
1034
|
shape
|
|
434
1035
|
end
|
|
435
1036
|
|
|
1037
|
+
def append_expanded_shape(shape, &block)
|
|
1038
|
+
shape_index = @params[:shapes].length
|
|
1039
|
+
register_shape_id!(shape, shape_index)
|
|
1040
|
+
@params[:shapes] << shape
|
|
1041
|
+
|
|
1042
|
+
with_shape_context(shape, shape_index) do
|
|
1043
|
+
instance_eval(&block) if block
|
|
1044
|
+
end
|
|
1045
|
+
apply_current_shape_group!(shape)
|
|
1046
|
+
validate_shape!(shape)
|
|
1047
|
+
|
|
1048
|
+
shape
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
def append_dynamic_custom_shape(renderer, options, shape_id:, &block)
|
|
1052
|
+
definition = custom_shape_definition(renderer)
|
|
1053
|
+
@type ||= :shape
|
|
1054
|
+
@params[:custom_shapes] ||= []
|
|
1055
|
+
descriptor = {
|
|
1056
|
+
name: definition.name || renderer,
|
|
1057
|
+
renderer: definition.renderer,
|
|
1058
|
+
params: deep_dup(options),
|
|
1059
|
+
style: {},
|
|
1060
|
+
transform: {},
|
|
1061
|
+
dynamic: true
|
|
1062
|
+
}
|
|
1063
|
+
param_schema = custom_shape_param_schema(definition.renderer)
|
|
1064
|
+
descriptor[:param_schema] = param_schema unless param_schema.empty?
|
|
1065
|
+
descriptor[:shape_id] = shape_id.to_sym if shape_id
|
|
1066
|
+
descriptor_index = @params[:custom_shapes].length
|
|
1067
|
+
@params[:custom_shapes] << descriptor
|
|
1068
|
+
|
|
1069
|
+
with_custom_shape_context(descriptor, descriptor_index) do
|
|
1070
|
+
instance_eval(&block) if block
|
|
1071
|
+
end
|
|
1072
|
+
apply_current_shape_group_to_custom_shape!(descriptor)
|
|
1073
|
+
|
|
1074
|
+
descriptor
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
def shape_options(id, options)
|
|
1078
|
+
return options if id.nil?
|
|
1079
|
+
|
|
1080
|
+
raise ArgumentError, "shape id specified twice" if options.key?(:id)
|
|
1081
|
+
|
|
1082
|
+
options.merge(id: id.to_sym)
|
|
1083
|
+
end
|
|
1084
|
+
|
|
436
1085
|
def normalize_shape(kind, options)
|
|
437
1086
|
shape = { kind: kind.to_sym }
|
|
438
1087
|
options.each do |key, value|
|
|
@@ -441,6 +1090,307 @@ module Vizcore
|
|
|
441
1090
|
shape
|
|
442
1091
|
end
|
|
443
1092
|
|
|
1093
|
+
def normalize_shape_group(attrs)
|
|
1094
|
+
attrs.each_with_object({}) do |(key, value), group|
|
|
1095
|
+
symbol_key = key.to_sym
|
|
1096
|
+
if SHAPE_TRANSFORM_KEYS.include?(symbol_key)
|
|
1097
|
+
transform_key = symbol_key == :rotation ? :rotate : symbol_key
|
|
1098
|
+
group[:transform] ||= {}
|
|
1099
|
+
group[:transform][transform_key] = value
|
|
1100
|
+
else
|
|
1101
|
+
group[symbol_key] = value
|
|
1102
|
+
end
|
|
1103
|
+
end
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
def merge_shape_group(parent, child)
|
|
1107
|
+
output = deep_dup(parent)
|
|
1108
|
+
child.each do |key, value|
|
|
1109
|
+
if key == :transform
|
|
1110
|
+
output[:transform] = compose_shape_transform(output[:transform], value)
|
|
1111
|
+
elsif key == :opacity && output.key?(:opacity)
|
|
1112
|
+
output[:opacity] = normalize_param_number(output[:opacity], :opacity) * normalize_param_number(value, :opacity)
|
|
1113
|
+
else
|
|
1114
|
+
output[key] = deep_dup(value)
|
|
1115
|
+
end
|
|
1116
|
+
end
|
|
1117
|
+
output
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
def apply_current_shape_group!(shape)
|
|
1121
|
+
group = current_shape_group
|
|
1122
|
+
return shape if group.empty?
|
|
1123
|
+
|
|
1124
|
+
SHAPE_STYLE_KEYS.each do |key|
|
|
1125
|
+
next unless group.key?(key)
|
|
1126
|
+
|
|
1127
|
+
if key == :opacity && shape.key?(:opacity)
|
|
1128
|
+
shape[:opacity] = normalize_param_number(group[:opacity], :opacity) * normalize_param_number(shape[:opacity], :opacity)
|
|
1129
|
+
else
|
|
1130
|
+
shape[key] = deep_dup(group[key]) unless shape.key?(key)
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
shape[:transform] = compose_shape_transform(group[:transform], shape[:transform]) if group[:transform]
|
|
1134
|
+
shape
|
|
1135
|
+
end
|
|
1136
|
+
|
|
1137
|
+
def apply_current_shape_group_to_custom_shape!(descriptor)
|
|
1138
|
+
group = current_shape_group
|
|
1139
|
+
return descriptor if group.empty?
|
|
1140
|
+
|
|
1141
|
+
style = descriptor[:style] ||= {}
|
|
1142
|
+
SHAPE_STYLE_KEYS.each do |key|
|
|
1143
|
+
next unless group.key?(key)
|
|
1144
|
+
|
|
1145
|
+
if key == :opacity && style.key?(:opacity)
|
|
1146
|
+
style[:opacity] = normalize_param_number(group[:opacity], :opacity) * normalize_param_number(style[:opacity], :opacity)
|
|
1147
|
+
else
|
|
1148
|
+
style[key] = deep_dup(group[key]) unless style.key?(key)
|
|
1149
|
+
end
|
|
1150
|
+
end
|
|
1151
|
+
descriptor[:transform] = compose_shape_transform(group[:transform], descriptor[:transform]) if group[:transform]
|
|
1152
|
+
descriptor
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def compose_shape_transform(parent, child)
|
|
1156
|
+
return deep_dup(child || {}) unless parent
|
|
1157
|
+
|
|
1158
|
+
child ||= {}
|
|
1159
|
+
output = deep_dup(parent)
|
|
1160
|
+
output[:translate] = add_shape_xy(parent[:translate], child[:translate]) if child.key?(:translate)
|
|
1161
|
+
output[:origin] = child[:origin] if child.key?(:origin)
|
|
1162
|
+
output[:rotate] = normalize_param_number(parent[:rotate] || 0, :rotate) + normalize_param_number(child[:rotate] || 0, :rotate) if child.key?(:rotate)
|
|
1163
|
+
output[:scale] = multiply_shape_scale(parent[:scale], child[:scale]) if child.key?(:scale)
|
|
1164
|
+
output
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
def add_shape_xy(parent, child)
|
|
1168
|
+
parent ||= {}
|
|
1169
|
+
child ||= {}
|
|
1170
|
+
{
|
|
1171
|
+
x: normalize_param_number(parent[:x] || parent["x"] || 0, :"translate.x") + normalize_param_number(child[:x] || child["x"] || 0, :"translate.x"),
|
|
1172
|
+
y: normalize_param_number(parent[:y] || parent["y"] || 0, :"translate.y") + normalize_param_number(child[:y] || child["y"] || 0, :"translate.y")
|
|
1173
|
+
}
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
def multiply_shape_scale(parent, child)
|
|
1177
|
+
parent = shape_scale_pair(parent)
|
|
1178
|
+
child = shape_scale_pair(child)
|
|
1179
|
+
{ x: parent[:x] * child[:x], y: parent[:y] * child[:y] }
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
def shape_scale_pair(value)
|
|
1183
|
+
return { x: normalize_param_number(value[:x] || value["x"] || 1, :"scale.x"), y: normalize_param_number(value[:y] || value["y"] || 1, :"scale.y") } if value.is_a?(Hash)
|
|
1184
|
+
|
|
1185
|
+
scale = normalize_param_number(value || 1, :scale)
|
|
1186
|
+
{ x: scale, y: scale }
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
def current_shape_group
|
|
1190
|
+
@shape_group_stack.last
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
def current_shape_group_transform
|
|
1194
|
+
current_shape_group[:transform] ||= {}
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1197
|
+
def current_custom_shape_style
|
|
1198
|
+
@current_custom_shape[:style] ||= {}
|
|
1199
|
+
end
|
|
1200
|
+
|
|
1201
|
+
def current_custom_shape_transform
|
|
1202
|
+
@current_custom_shape[:transform] ||= {}
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
def in_shape_group?
|
|
1206
|
+
@shape_group_stack.length > 1
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
def validate_shape!(shape)
|
|
1210
|
+
validate_non_negative_shape_numbers!(shape)
|
|
1211
|
+
case shape.fetch(:kind)
|
|
1212
|
+
when :polygon
|
|
1213
|
+
validate_shape_points!(shape, minimum: 3)
|
|
1214
|
+
when :polyline
|
|
1215
|
+
validate_shape_points!(shape, minimum: 2)
|
|
1216
|
+
when :path
|
|
1217
|
+
validate_path_shape!(shape)
|
|
1218
|
+
end
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
def validate_path_shape!(shape)
|
|
1222
|
+
commands = Array(shape[:commands])
|
|
1223
|
+
raise ArgumentError, "Invalid path#{shape_label(shape)}: commands must not be empty" if commands.empty?
|
|
1224
|
+
|
|
1225
|
+
detail = normalized_path_integer(shape, :detail, PATH_DEFAULT_DETAIL).clamp(PATH_MIN_DETAIL, PATH_MAX_DETAIL)
|
|
1226
|
+
max_segments = normalized_path_integer(shape, :max_segments, PATH_DEFAULT_MAX_SEGMENTS)
|
|
1227
|
+
validate_path_tolerance!(shape)
|
|
1228
|
+
|
|
1229
|
+
segment_count = estimated_path_segments(commands, detail)
|
|
1230
|
+
return if segment_count <= max_segments
|
|
1231
|
+
|
|
1232
|
+
raise ArgumentError,
|
|
1233
|
+
"Invalid path#{shape_label(shape)}: max_segments exceeded (#{segment_count} > #{max_segments})"
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
def normalized_path_integer(shape, key, default)
|
|
1237
|
+
value = shape.key?(key) ? shape[key] : default
|
|
1238
|
+
numeric = Integer(value)
|
|
1239
|
+
raise ArgumentError if numeric <= 0
|
|
1240
|
+
|
|
1241
|
+
numeric
|
|
1242
|
+
rescue ArgumentError, TypeError
|
|
1243
|
+
raise ArgumentError, "Invalid path#{shape_label(shape)}: #{key} must be a positive integer"
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
def validate_path_tolerance!(shape)
|
|
1247
|
+
return unless shape.key?(:tolerance)
|
|
1248
|
+
|
|
1249
|
+
value = normalize_param_number(shape[:tolerance], :tolerance)
|
|
1250
|
+
return unless value.negative?
|
|
1251
|
+
|
|
1252
|
+
raise ArgumentError, "Invalid path#{shape_label(shape)}: tolerance must be non-negative"
|
|
1253
|
+
end
|
|
1254
|
+
|
|
1255
|
+
def estimated_path_segments(commands, detail)
|
|
1256
|
+
current = false
|
|
1257
|
+
subpath_start = false
|
|
1258
|
+
commands.sum do |entry|
|
|
1259
|
+
command, *values = Array(entry)
|
|
1260
|
+
case command.to_s.upcase
|
|
1261
|
+
when "M"
|
|
1262
|
+
current = values.length >= 2
|
|
1263
|
+
subpath_start = current
|
|
1264
|
+
0
|
|
1265
|
+
when "L"
|
|
1266
|
+
current && values.length >= 2 ? 1 : 0
|
|
1267
|
+
when "H", "V"
|
|
1268
|
+
current && values.length >= 1 ? 1 : 0
|
|
1269
|
+
when "Q"
|
|
1270
|
+
current && values.length >= 4 ? detail : 0
|
|
1271
|
+
when "C"
|
|
1272
|
+
current && values.length >= 6 ? detail : 0
|
|
1273
|
+
when "A"
|
|
1274
|
+
current && values.length >= 7 ? detail : 0
|
|
1275
|
+
when "Z"
|
|
1276
|
+
current && subpath_start ? 1 : 0
|
|
1277
|
+
else
|
|
1278
|
+
0
|
|
1279
|
+
end
|
|
1280
|
+
end
|
|
1281
|
+
end
|
|
1282
|
+
|
|
1283
|
+
def validate_non_negative_shape_numbers!(shape)
|
|
1284
|
+
%i[radius width height stroke_width inner_radius].each do |key|
|
|
1285
|
+
next unless shape.key?(key)
|
|
1286
|
+
|
|
1287
|
+
value = normalize_param_number(shape[key], key)
|
|
1288
|
+
raise ArgumentError, "Invalid #{shape.fetch(:kind)}#{shape_label(shape)}: #{key} must be non-negative" if value.negative?
|
|
1289
|
+
end
|
|
1290
|
+
end
|
|
1291
|
+
|
|
1292
|
+
def validate_shape_points!(shape, minimum:)
|
|
1293
|
+
points = Array(shape[:points])
|
|
1294
|
+
valid_points = points.count { |point| Array(point).length >= 2 }
|
|
1295
|
+
return if valid_points >= minimum
|
|
1296
|
+
|
|
1297
|
+
raise ArgumentError, "Invalid #{shape.fetch(:kind)}#{shape_label(shape)}: points must contain at least #{minimum} points"
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
def shape_label(shape)
|
|
1301
|
+
shape[:id] ? " `#{shape[:id]}`" : ""
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
def expand_custom_shape(renderer, options, shape_id:, cache: false)
|
|
1305
|
+
definition = custom_shape_definition(renderer)
|
|
1306
|
+
Vizcore::Shape.expand_custom_shape(
|
|
1307
|
+
definition.renderer,
|
|
1308
|
+
params: options,
|
|
1309
|
+
shape_id: shape_id,
|
|
1310
|
+
layer_name: @name,
|
|
1311
|
+
palette: Array(@params[:palette]),
|
|
1312
|
+
shape_name: definition.name || renderer,
|
|
1313
|
+
cache: cache
|
|
1314
|
+
)
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1317
|
+
def custom_shape_definition(renderer)
|
|
1318
|
+
return Vizcore::Shape::Definition.new(name: nil, renderer: renderer) unless renderer.is_a?(Symbol) || renderer.is_a?(String)
|
|
1319
|
+
|
|
1320
|
+
Vizcore.resolve_shape(renderer) || raise(ArgumentError, "Unknown custom shape: #{renderer.inspect}. Register it with `Vizcore.register_shape #{renderer.inspect}, ShapeClass`.")
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
def custom_shape_param_schema(renderer)
|
|
1324
|
+
return [] unless renderer.respond_to?(:shape_param_schema)
|
|
1325
|
+
|
|
1326
|
+
renderer.shape_param_schema.values.map(&:dup)
|
|
1327
|
+
end
|
|
1328
|
+
|
|
1329
|
+
def register_shape_id!(shape, shape_index)
|
|
1330
|
+
id = shape[:id]
|
|
1331
|
+
return if id.nil?
|
|
1332
|
+
|
|
1333
|
+
key = id.to_sym
|
|
1334
|
+
raise ArgumentError, "duplicate shape id: #{key.inspect}" if @shape_index_by_id.key?(key)
|
|
1335
|
+
|
|
1336
|
+
@shape_index_by_id[key] = shape_index
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
def current_shape_transform
|
|
1340
|
+
mark_shape_schema_version!
|
|
1341
|
+
@current_shape[:transform] ||= {}
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
def mark_shape_schema_version!
|
|
1345
|
+
@params[:shape_schema_version] ||= SHAPE_SCHEMA_VERSION
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
def normalize_xy_args(args, x:, y:, name:)
|
|
1349
|
+
if args.length == 2
|
|
1350
|
+
return { x: normalize_param_number(args[0], :"#{name}.x"), y: normalize_param_number(args[1], :"#{name}.y") }
|
|
1351
|
+
end
|
|
1352
|
+
|
|
1353
|
+
if args.length == 1 && args.first.is_a?(Hash)
|
|
1354
|
+
values = args.first
|
|
1355
|
+
x = values.fetch(:x, values["x"])
|
|
1356
|
+
y = values.fetch(:y, values["y"])
|
|
1357
|
+
elsif args.any?
|
|
1358
|
+
raise ArgumentError, "#{name} expects x/y keywords or two numeric arguments"
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
{
|
|
1362
|
+
x: normalize_param_number(x || 0, :"#{name}.x"),
|
|
1363
|
+
y: normalize_param_number(y || 0, :"#{name}.y")
|
|
1364
|
+
}
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1367
|
+
def normalize_scale_args(value, x:, y:)
|
|
1368
|
+
if value.equal?(NO_ARGUMENT)
|
|
1369
|
+
return {
|
|
1370
|
+
x: normalize_param_number(x || 1, :"scale.x"),
|
|
1371
|
+
y: normalize_param_number(y || 1, :"scale.y")
|
|
1372
|
+
}
|
|
1373
|
+
end
|
|
1374
|
+
|
|
1375
|
+
raise ArgumentError, "scale accepts either a value or x/y keywords" unless x.nil? && y.nil?
|
|
1376
|
+
|
|
1377
|
+
normalize_param_number(value, :scale)
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
def append_path_command(command, *values)
|
|
1381
|
+
raise ArgumentError, "#{command} is only available inside a path shape" unless @current_shape&.fetch(:kind) == :path
|
|
1382
|
+
|
|
1383
|
+
@current_shape[:commands] ||= []
|
|
1384
|
+
@current_shape[:commands] << [command, *values]
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
def point_values(value)
|
|
1388
|
+
values = Array(value)
|
|
1389
|
+
raise ArgumentError, "point must contain x and y" unless values.length == 2
|
|
1390
|
+
|
|
1391
|
+
values
|
|
1392
|
+
end
|
|
1393
|
+
|
|
444
1394
|
def with_shape_context(shape, shape_index)
|
|
445
1395
|
previous_shape = @current_shape
|
|
446
1396
|
previous_prefix = @shape_target_prefix
|
|
@@ -452,6 +1402,50 @@ module Vizcore
|
|
|
452
1402
|
@shape_target_prefix = previous_prefix
|
|
453
1403
|
end
|
|
454
1404
|
|
|
1405
|
+
def with_custom_shape_context(descriptor, descriptor_index)
|
|
1406
|
+
previous_custom_shape = @current_custom_shape
|
|
1407
|
+
previous_prefix = @custom_shape_target_prefix
|
|
1408
|
+
@current_custom_shape = descriptor
|
|
1409
|
+
@custom_shape_target_prefix = "custom_shapes.#{descriptor_index}"
|
|
1410
|
+
yield
|
|
1411
|
+
ensure
|
|
1412
|
+
@current_custom_shape = previous_custom_shape
|
|
1413
|
+
@custom_shape_target_prefix = previous_prefix
|
|
1414
|
+
end
|
|
1415
|
+
|
|
1416
|
+
def normalize_custom_shape_mapping(definition, options)
|
|
1417
|
+
if options.key?(:to)
|
|
1418
|
+
prefixed_options = options.dup
|
|
1419
|
+
prefixed_options[:to] = prefixed_custom_shape_target(prefixed_options[:to])
|
|
1420
|
+
return [definition, prefixed_options]
|
|
1421
|
+
end
|
|
1422
|
+
|
|
1423
|
+
mapping = definition.nil? ? options : Hash(definition)
|
|
1424
|
+
prefixed_mapping = mapping.each_with_object({}) do |(source, target), output|
|
|
1425
|
+
output[source] = prefix_custom_shape_target_value(target)
|
|
1426
|
+
end
|
|
1427
|
+
[prefixed_mapping, {}]
|
|
1428
|
+
end
|
|
1429
|
+
|
|
1430
|
+
def prefix_custom_shape_target_value(target)
|
|
1431
|
+
return prefixed_custom_shape_target(target) unless target.is_a?(Hash)
|
|
1432
|
+
|
|
1433
|
+
target.merge(to: prefixed_custom_shape_target(target.fetch(:to)))
|
|
1434
|
+
rescue KeyError
|
|
1435
|
+
target
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1438
|
+
def prefixed_custom_shape_target(target)
|
|
1439
|
+
target_name = target.to_s
|
|
1440
|
+
return :"#{@custom_shape_target_prefix}.#{target_name}" if target_name.match?(/\A(?:params|style|transform)\./)
|
|
1441
|
+
|
|
1442
|
+
resolved_target = SHAPE_TARGET_ALIASES[target_name]
|
|
1443
|
+
return :"#{@custom_shape_target_prefix}.#{resolved_target}" if resolved_target
|
|
1444
|
+
return :"#{@custom_shape_target_prefix}.style.#{target_name}" if SHAPE_STYLE_KEYS.include?(target_name.to_sym)
|
|
1445
|
+
|
|
1446
|
+
:"#{@custom_shape_target_prefix}.params.#{target_name}"
|
|
1447
|
+
end
|
|
1448
|
+
|
|
455
1449
|
def normalize_shape_mapping(definition, options)
|
|
456
1450
|
if options.key?(:to)
|
|
457
1451
|
prefixed_options = options.dup
|
|
@@ -475,7 +1469,9 @@ module Vizcore
|
|
|
475
1469
|
end
|
|
476
1470
|
|
|
477
1471
|
def prefixed_shape_target(target)
|
|
478
|
-
|
|
1472
|
+
target_name = target.to_s
|
|
1473
|
+
resolved_target = SHAPE_TARGET_ALIASES.fetch(target_name, target_name)
|
|
1474
|
+
:"#{@shape_target_prefix}.#{resolved_target}"
|
|
479
1475
|
end
|
|
480
1476
|
|
|
481
1477
|
def resolved_type
|
|
@@ -534,16 +1530,7 @@ module Vizcore
|
|
|
534
1530
|
end
|
|
535
1531
|
|
|
536
1532
|
def deep_dup(value)
|
|
537
|
-
|
|
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
|
|
1533
|
+
Vizcore::DeepCopy.copy(value)
|
|
547
1534
|
end
|
|
548
1535
|
|
|
549
1536
|
def evaluate_transform_block(initial_options, &block)
|
|
@@ -577,19 +1564,26 @@ module Vizcore
|
|
|
577
1564
|
raise ArgumentError, "param min must be less than or equal to max"
|
|
578
1565
|
end
|
|
579
1566
|
|
|
580
|
-
def normalize_transform(gain: nil, range: nil, min: nil, max: nil, curve: nil, attack: nil, release: nil, deadzone: nil)
|
|
1567
|
+
def normalize_transform(gain: nil, range: nil, min: nil, max: nil, curve: nil, attack: nil, release: nil, deadzone: nil, threshold: nil, hysteresis: nil, hold: nil, decay: nil, cooldown: nil, one_shot: nil, as: nil)
|
|
581
1568
|
range_min, range_max = normalize_range(range, context: "mapping")
|
|
582
1569
|
min = range_min if min.nil?
|
|
583
1570
|
max = range_max if max.nil?
|
|
584
1571
|
|
|
585
1572
|
output = {}
|
|
586
1573
|
output[:deadzone] = normalize_non_negative_float(deadzone, :deadzone) unless deadzone.nil?
|
|
1574
|
+
output[:as] = normalize_mapping_mode(as) unless as.nil?
|
|
1575
|
+
output[:threshold] = normalize_float(threshold, :threshold) unless threshold.nil?
|
|
1576
|
+
output[:hysteresis] = normalize_non_negative_float(hysteresis, :hysteresis) unless hysteresis.nil?
|
|
587
1577
|
output[:gain] = normalize_float(gain, :gain) unless gain.nil?
|
|
588
1578
|
output[:min] = normalize_float(min, :min) unless min.nil?
|
|
589
1579
|
output[:max] = normalize_float(max, :max) unless max.nil?
|
|
590
1580
|
output[:curve] = normalize_curve(curve) unless curve.nil?
|
|
591
1581
|
output[:attack] = clamp(normalize_float(attack, :attack), 0.0, 1.0) unless attack.nil?
|
|
592
1582
|
output[:release] = clamp(normalize_float(release, :release), 0.0, 1.0) unless release.nil?
|
|
1583
|
+
output[:hold] = normalize_non_negative_float(hold, :hold) unless hold.nil?
|
|
1584
|
+
output[:decay] = clamp(normalize_float(decay, :decay), 0.0, 1.0) unless decay.nil?
|
|
1585
|
+
output[:cooldown] = normalize_non_negative_float(cooldown, :cooldown) unless cooldown.nil?
|
|
1586
|
+
output[:one_shot] = !!one_shot unless one_shot.nil?
|
|
593
1587
|
output
|
|
594
1588
|
end
|
|
595
1589
|
|
|
@@ -631,11 +1625,30 @@ module Vizcore
|
|
|
631
1625
|
|
|
632
1626
|
def normalize_curve(value)
|
|
633
1627
|
curve = value.to_sym
|
|
634
|
-
return curve if %i[linear sqrt square ease_out].include?(curve)
|
|
1628
|
+
return curve if %i[linear sqrt square ease_out ease_in ease_in_out smoothstep exp log step].include?(curve)
|
|
635
1629
|
|
|
636
1630
|
raise ArgumentError, "unsupported mapping curve: #{value.inspect}"
|
|
637
1631
|
end
|
|
638
1632
|
|
|
1633
|
+
def normalize_mapping_mode(value)
|
|
1634
|
+
mode = value.to_sym
|
|
1635
|
+
return mode if %i[continuous trigger].include?(mode)
|
|
1636
|
+
|
|
1637
|
+
raise ArgumentError, "unsupported mapping mode: #{value.inspect}"
|
|
1638
|
+
end
|
|
1639
|
+
|
|
1640
|
+
def validate_strict_params!
|
|
1641
|
+
unknown = @params.keys.map(&:to_sym) - strict_allowed_params
|
|
1642
|
+
return if unknown.empty?
|
|
1643
|
+
|
|
1644
|
+
raise ArgumentError, "layer #{@name} has unknown params in strict mode: #{unknown.sort.join(', ')}"
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1647
|
+
def strict_allowed_params
|
|
1648
|
+
catalog_params = Vizcore::LayerCatalog.params_for(resolved_type).keys
|
|
1649
|
+
(catalog_params + @param_schema.keys + STRICT_PARAM_ALLOWLIST).map(&:to_sym).uniq
|
|
1650
|
+
end
|
|
1651
|
+
|
|
639
1652
|
def clamp(value, min, max)
|
|
640
1653
|
[[value, min].max, max].min
|
|
641
1654
|
end
|
|
@@ -646,6 +1659,44 @@ module Vizcore
|
|
|
646
1659
|
**options
|
|
647
1660
|
}
|
|
648
1661
|
end
|
|
1662
|
+
|
|
1663
|
+
def shape_id_suggestions(key)
|
|
1664
|
+
return "" if @shape_index_by_id.empty?
|
|
1665
|
+
|
|
1666
|
+
candidates = @shape_index_by_id.keys
|
|
1667
|
+
.map do |shape_id|
|
|
1668
|
+
[shape_id, levenshtein_distance(shape_id.to_s, key.to_s)]
|
|
1669
|
+
end
|
|
1670
|
+
.select { |_, distance| distance <= 3 }
|
|
1671
|
+
.sort_by { |shape_id, distance| [distance, shape_id.to_s] }
|
|
1672
|
+
.first(3)
|
|
1673
|
+
|
|
1674
|
+
return "" if candidates.empty?
|
|
1675
|
+
|
|
1676
|
+
candidates.map { |shape_id, _| shape_id.inspect }.join(", ")
|
|
1677
|
+
end
|
|
1678
|
+
|
|
1679
|
+
def levenshtein_distance(a, b)
|
|
1680
|
+
prev = (0..b.length).to_a
|
|
1681
|
+
b_chars = b.bytes
|
|
1682
|
+
a_bytes = a.bytes
|
|
1683
|
+
|
|
1684
|
+
a_bytes.each_with_index do |codepoint_a, index_a|
|
|
1685
|
+
current = [index_a + 1]
|
|
1686
|
+
b_chars.each_with_index do |codepoint_b, index_b|
|
|
1687
|
+
cost = codepoint_a == codepoint_b ? 0 : 1
|
|
1688
|
+
current << [
|
|
1689
|
+
current[index_b] + 1,
|
|
1690
|
+
prev[index_b + 1] + 1,
|
|
1691
|
+
prev[index_b] + cost
|
|
1692
|
+
].min
|
|
1693
|
+
end
|
|
1694
|
+
|
|
1695
|
+
prev = current
|
|
1696
|
+
end
|
|
1697
|
+
|
|
1698
|
+
prev[b.length]
|
|
1699
|
+
end
|
|
649
1700
|
end
|
|
650
1701
|
end
|
|
651
1702
|
end
|