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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../deep_copy"
|
|
3
4
|
require_relative "../shape"
|
|
4
5
|
|
|
5
6
|
module Vizcore
|
|
@@ -10,21 +11,29 @@ module Vizcore
|
|
|
10
11
|
@mapping_state = {}
|
|
11
12
|
end
|
|
12
13
|
|
|
14
|
+
# Clear stateful transform memory such as smoothing, hold, decay, and hysteresis.
|
|
15
|
+
#
|
|
16
|
+
# @return [void]
|
|
17
|
+
def reset!
|
|
18
|
+
@mapping_state.clear
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
# @param scene_layers [Array<Hash>]
|
|
14
22
|
# @param audio [Hash]
|
|
15
23
|
# @return [Array<Hash>] normalized layer payloads with resolved params
|
|
16
|
-
def resolve_layers(scene_layers:, audio:, time: 0.0, frame: 0, resolution: [1280, 720], globals: {}, custom_shape_overrides: {})
|
|
24
|
+
def resolve_layers(scene_layers:, audio:, time: 0.0, frame: 0, resolution: [1280, 720], globals: {}, custom_shape_overrides: {}, layer_param_overrides: {})
|
|
17
25
|
normalize_scene_layers(scene_layers).map do |layer|
|
|
18
|
-
resolve_layer(layer, audio, time: time, frame: frame, resolution: resolution, globals: globals, custom_shape_overrides: custom_shape_overrides)
|
|
26
|
+
resolve_layer(layer, audio, time: time, frame: frame, resolution: resolution, globals: globals, custom_shape_overrides: custom_shape_overrides, layer_param_overrides: layer_param_overrides)
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
private
|
|
23
31
|
|
|
24
|
-
def resolve_layer(layer, audio, time:, frame:, resolution:, globals:, custom_shape_overrides:)
|
|
32
|
+
def resolve_layer(layer, audio, time:, frame:, resolution:, globals:, custom_shape_overrides:, layer_param_overrides:)
|
|
25
33
|
params = deep_dup(layer[:params] || {})
|
|
26
34
|
apply_custom_shape_overrides!(params, layer_name: layer[:name], custom_shape_overrides: custom_shape_overrides)
|
|
27
|
-
merge_resolved_mappings!(params, resolve_mappings(layer[:mappings], audio, layer_name: layer[:name]))
|
|
35
|
+
merge_resolved_mappings!(params, resolve_mappings(layer[:mappings], audio, globals: globals, layer_name: layer[:name], time: time, frame: frame))
|
|
36
|
+
apply_layer_param_overrides!(params, layer_name: layer[:name], layer_param_overrides: layer_param_overrides)
|
|
28
37
|
expand_dynamic_custom_shapes!(params, layer: layer, audio: audio, time: time, frame: frame, resolution: resolution, globals: globals)
|
|
29
38
|
|
|
30
39
|
output = {
|
|
@@ -39,14 +48,22 @@ module Vizcore
|
|
|
39
48
|
output
|
|
40
49
|
end
|
|
41
50
|
|
|
42
|
-
def resolve_mappings(mappings, audio, layer_name:)
|
|
51
|
+
def resolve_mappings(mappings, audio, globals:, layer_name:, time:, frame:)
|
|
43
52
|
Array(mappings).each_with_object({}) do |mapping, resolved|
|
|
44
53
|
source = mapping[:source]
|
|
45
54
|
target = mapping[:target]
|
|
46
55
|
next unless source && target
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
value =
|
|
57
|
+
state_key = [layer_name, target, source]
|
|
58
|
+
value = resolve_source_value(
|
|
59
|
+
source,
|
|
60
|
+
audio,
|
|
61
|
+
globals: globals,
|
|
62
|
+
time: time,
|
|
63
|
+
state_key: state_key,
|
|
64
|
+
frame: frame
|
|
65
|
+
)
|
|
66
|
+
value = apply_transform(value, mapping[:transform], state_key: state_key, frame: frame)
|
|
50
67
|
resolved[target.to_s] = value unless value.nil?
|
|
51
68
|
end
|
|
52
69
|
end
|
|
@@ -134,6 +151,18 @@ module Vizcore
|
|
|
134
151
|
{}
|
|
135
152
|
end
|
|
136
153
|
|
|
154
|
+
def apply_layer_param_overrides!(params, layer_name:, layer_param_overrides:)
|
|
155
|
+
layer_overrides = custom_shape_layer_overrides(layer_param_overrides, layer_name)
|
|
156
|
+
layer_overrides.each do |target, value|
|
|
157
|
+
target_name = target.to_s
|
|
158
|
+
if target_name.include?(".")
|
|
159
|
+
assign_nested_param(params, target_name.split("."), value)
|
|
160
|
+
else
|
|
161
|
+
params[target_name.to_sym] = value
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
137
166
|
def apply_custom_shape_attributes!(primitive, descriptor)
|
|
138
167
|
style = Hash(descriptor[:style] || {})
|
|
139
168
|
style.each do |key, value|
|
|
@@ -230,12 +259,18 @@ module Vizcore
|
|
|
230
259
|
raise ArgumentError, "param #{name} must be numeric"
|
|
231
260
|
end
|
|
232
261
|
|
|
233
|
-
def resolve_source_value(source, audio)
|
|
262
|
+
def resolve_source_value(source, audio, globals: {}, time: 0.0, state_key: nil, frame: 0)
|
|
263
|
+
return 0.0 unless source
|
|
264
|
+
|
|
234
265
|
case source[:kind]&.to_sym
|
|
235
266
|
when :amplitude
|
|
236
267
|
audio[:amplitude]
|
|
268
|
+
when :peak
|
|
269
|
+
audio[:peak]
|
|
237
270
|
when :frequency_band
|
|
238
271
|
audio.dig(:bands, source[:band]&.to_sym)
|
|
272
|
+
when :frequency_band_peak
|
|
273
|
+
audio.dig(:band_peaks, source[:band]&.to_sym)
|
|
239
274
|
when :fft_spectrum
|
|
240
275
|
audio[:fft]
|
|
241
276
|
when :onset
|
|
@@ -250,13 +285,252 @@ module Vizcore
|
|
|
250
285
|
audio[:beat_pulse]
|
|
251
286
|
when :beat_count
|
|
252
287
|
audio[:beat_count]
|
|
288
|
+
when :beat_phase
|
|
289
|
+
audio[:beat_phase]
|
|
290
|
+
when :beat_2
|
|
291
|
+
audio[:beat_2]
|
|
292
|
+
when :beat_4
|
|
293
|
+
audio[:beat_4]
|
|
294
|
+
when :beat_8
|
|
295
|
+
audio[:beat_8]
|
|
296
|
+
when :beat_triplet, :triplet
|
|
297
|
+
audio[:beat_triplet]
|
|
298
|
+
when :bar_phase
|
|
299
|
+
audio[:bar_phase]
|
|
300
|
+
when :bar_count
|
|
301
|
+
audio[:bar_count]
|
|
302
|
+
when :phrase_count
|
|
303
|
+
audio[:phrase_count]
|
|
253
304
|
when :bpm
|
|
254
305
|
audio[:bpm]
|
|
306
|
+
when :bpm_confidence
|
|
307
|
+
audio[:bpm_confidence]
|
|
308
|
+
when :spectral_centroid
|
|
309
|
+
audio[:spectral_centroid]
|
|
310
|
+
when :spectral_rolloff
|
|
311
|
+
audio[:spectral_rolloff]
|
|
312
|
+
when :spectral_flatness
|
|
313
|
+
audio[:spectral_flatness]
|
|
314
|
+
when :spectral_flux
|
|
315
|
+
audio[:spectral_flux]
|
|
316
|
+
when :zero_crossing_rate
|
|
317
|
+
audio[:zero_crossing_rate]
|
|
318
|
+
when :global
|
|
319
|
+
resolve_global(source, globals)
|
|
320
|
+
when :lfo
|
|
321
|
+
resolve_lfo(source, time)
|
|
322
|
+
when :adsr, :envelope
|
|
323
|
+
resolve_envelope(source, audio, globals: globals, time: time, state_key: state_key, frame: frame)
|
|
255
324
|
else
|
|
256
325
|
nil
|
|
257
326
|
end
|
|
258
327
|
end
|
|
259
328
|
|
|
329
|
+
def resolve_envelope(source, audio, globals: {}, time: 0.0, state_key:, frame:)
|
|
330
|
+
state = envelope_state(state_key)
|
|
331
|
+
params = envelope_params(source)
|
|
332
|
+
nested = source[:source] || :kick
|
|
333
|
+
normalized_nested = normalize_source_descriptor(nested)
|
|
334
|
+
trigger_value = resolve_nested_source_value(normalized_nested, audio, globals: globals, time: time)
|
|
335
|
+
trigger = trigger_numeric(trigger_value)
|
|
336
|
+
|
|
337
|
+
now = normalized_time(time)
|
|
338
|
+
state[:time] = now
|
|
339
|
+
state[:last_frame] = frame
|
|
340
|
+
state[:gate] = trigger > params[:threshold]
|
|
341
|
+
state[:note_on] = state[:gate]
|
|
342
|
+
|
|
343
|
+
peak = normalize_envelope_peak(params.fetch(:peak)) * trigger
|
|
344
|
+
if state[:gate]
|
|
345
|
+
if state[:phase] == :idle || state[:phase] == :release
|
|
346
|
+
state[:phase] = :attack
|
|
347
|
+
state[:phase_started_at] = now
|
|
348
|
+
state[:phase_start_value] = state[:value]
|
|
349
|
+
state[:peak] = peak
|
|
350
|
+
else
|
|
351
|
+
state[:peak] = [state[:peak], peak].max
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
state[:value], state[:phase] = next_envelope_step(
|
|
356
|
+
state,
|
|
357
|
+
params: params,
|
|
358
|
+
now: now
|
|
359
|
+
)
|
|
360
|
+
state[:value]
|
|
361
|
+
rescue StandardError
|
|
362
|
+
0.0
|
|
363
|
+
ensure
|
|
364
|
+
@mapping_state[state_key] = state if state_key
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def resolve_nested_source_value(source, audio, globals:, time:)
|
|
368
|
+
return resolve_source_value({ kind: :amplitude }, audio, globals: globals, time: time) if source.nil?
|
|
369
|
+
|
|
370
|
+
nested_kind = source[:kind]&.to_sym
|
|
371
|
+
return 0.0 if nested_kind == :adsr || nested_kind == :envelope
|
|
372
|
+
|
|
373
|
+
resolve_source_value(source, audio, globals: globals, time: time)
|
|
374
|
+
rescue StandardError
|
|
375
|
+
0.0
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def normalize_source_descriptor(source)
|
|
379
|
+
return source if source.is_a?(Hash) && source[:kind]
|
|
380
|
+
|
|
381
|
+
{ kind: source.to_sym }
|
|
382
|
+
rescue StandardError
|
|
383
|
+
nil
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def resolve_lfo(source, time)
|
|
387
|
+
rate = Float(source[:rate] || 1.0)
|
|
388
|
+
phase = Float(source[:phase] || 0.0)
|
|
389
|
+
position = (Float(time) * rate + phase) % 1.0
|
|
390
|
+
case source[:wave]&.to_sym
|
|
391
|
+
when :triangle
|
|
392
|
+
1.0 - ((position * 2.0) - 1.0).abs
|
|
393
|
+
when :saw
|
|
394
|
+
position
|
|
395
|
+
when :square
|
|
396
|
+
position < 0.5 ? 1.0 : 0.0
|
|
397
|
+
else
|
|
398
|
+
(Math.sin(position * Math::PI * 2.0) + 1.0) * 0.5
|
|
399
|
+
end
|
|
400
|
+
rescue ArgumentError, TypeError
|
|
401
|
+
0.0
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def resolve_global(source, globals)
|
|
405
|
+
name = source[:name]&.to_sym
|
|
406
|
+
return nil unless name
|
|
407
|
+
|
|
408
|
+
values = Hash(globals || {})
|
|
409
|
+
values[name] || values[name.to_s]
|
|
410
|
+
rescue StandardError
|
|
411
|
+
nil
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def envelope_state(state_key)
|
|
415
|
+
return {} unless state_key
|
|
416
|
+
|
|
417
|
+
@mapping_state[state_key] ||= {
|
|
418
|
+
phase: :idle,
|
|
419
|
+
value: 0.0,
|
|
420
|
+
peak: 0.0,
|
|
421
|
+
phase_started_at: 0.0,
|
|
422
|
+
phase_start_value: 0.0,
|
|
423
|
+
time: 0.0,
|
|
424
|
+
note_on: false,
|
|
425
|
+
gate: false
|
|
426
|
+
}
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def envelope_params(source)
|
|
430
|
+
{
|
|
431
|
+
attack: Float(source[:attack] || 0.02),
|
|
432
|
+
decay: Float(source[:decay] || 0.08),
|
|
433
|
+
sustain: Float(source[:sustain] || 0.7).clamp(0.0, 1.0),
|
|
434
|
+
release: Float(source[:release] || 0.16),
|
|
435
|
+
threshold: Float(source[:threshold] || 0.0),
|
|
436
|
+
peak: Float(source[:peak] || 1.0)
|
|
437
|
+
}
|
|
438
|
+
rescue StandardError
|
|
439
|
+
{ attack: 0.02, decay: 0.08, sustain: 0.7, release: 0.16, threshold: 0.0, peak: 1.0 }
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def normalized_time(value)
|
|
443
|
+
numeric = Float(value)
|
|
444
|
+
numeric.nan? ? 0.0 : numeric
|
|
445
|
+
rescue StandardError
|
|
446
|
+
0.0
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def normalize_envelope_peak(value)
|
|
450
|
+
value = Float(value)
|
|
451
|
+
value.nan? ? 1.0 : value
|
|
452
|
+
rescue StandardError
|
|
453
|
+
1.0
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def next_envelope_step(state, params:, now:)
|
|
457
|
+
phase = state[:phase] || :idle
|
|
458
|
+
if phase == :attack
|
|
459
|
+
return [state[:peak], :sustain] if params[:attack] <= 0.0
|
|
460
|
+
|
|
461
|
+
elapsed = now - state[:phase_started_at]
|
|
462
|
+
if elapsed >= params[:attack]
|
|
463
|
+
state[:phase_started_at] = now
|
|
464
|
+
state[:phase_start_value] = state[:peak]
|
|
465
|
+
return [state[:peak], :decay]
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
ratio = [elapsed / params[:attack], 1.0].min
|
|
469
|
+
value = state[:phase_start_value] + (state[:peak] - state[:phase_start_value]) * ratio
|
|
470
|
+
return [value, :attack]
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
if phase == :decay
|
|
474
|
+
return [state[:peak] * params[:sustain], :sustain] if params[:decay] <= 0.0
|
|
475
|
+
|
|
476
|
+
elapsed = now - state[:phase_started_at]
|
|
477
|
+
target = state[:peak] * params[:sustain]
|
|
478
|
+
if elapsed >= params[:decay]
|
|
479
|
+
state[:phase_started_at] = now
|
|
480
|
+
state[:phase_start_value] = target
|
|
481
|
+
return [target, :sustain]
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
ratio = [elapsed / params[:decay], 1.0].min
|
|
485
|
+
value = state[:phase_start_value] + (target - state[:phase_start_value]) * ratio
|
|
486
|
+
return [value, :decay]
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
if phase == :sustain
|
|
490
|
+
return state[:phase_start_value], :sustain if state[:gate]
|
|
491
|
+
|
|
492
|
+
state[:phase] = :release
|
|
493
|
+
state[:phase_started_at] = now
|
|
494
|
+
state[:phase_start_value] = state[:value]
|
|
495
|
+
return [state[:value], :release]
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
if phase == :release
|
|
499
|
+
return [0.0, :idle] if params[:release] <= 0.0
|
|
500
|
+
|
|
501
|
+
elapsed = now - state[:phase_started_at]
|
|
502
|
+
if elapsed >= params[:release]
|
|
503
|
+
state[:phase_started_at] = now
|
|
504
|
+
state[:phase_start_value] = 0.0
|
|
505
|
+
return [0.0, :idle]
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
ratio = [elapsed / params[:release], 1.0].min
|
|
509
|
+
value = state[:phase_start_value] * (1.0 - ratio)
|
|
510
|
+
return [value, :release]
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
if phase == :idle
|
|
514
|
+
return [0.0, :idle] unless state[:gate] && params[:attack] > 0.0
|
|
515
|
+
|
|
516
|
+
state[:phase_started_at] = now
|
|
517
|
+
state[:phase_start_value] = 0.0
|
|
518
|
+
return [0.0, :attack]
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
[0.0, :idle]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def trigger_numeric(value)
|
|
525
|
+
return 0.0 if value == false || value == 0
|
|
526
|
+
return 1.0 if value == true
|
|
527
|
+
return 0.0 if value.nil?
|
|
528
|
+
|
|
529
|
+
Float(value)
|
|
530
|
+
rescue StandardError
|
|
531
|
+
0.0
|
|
532
|
+
end
|
|
533
|
+
|
|
260
534
|
def resolve_onset(source, audio)
|
|
261
535
|
band = source[:band]&.to_sym
|
|
262
536
|
return audio[:onset] unless band
|
|
@@ -264,15 +538,18 @@ module Vizcore
|
|
|
264
538
|
audio.dig(:onsets, band)
|
|
265
539
|
end
|
|
266
540
|
|
|
267
|
-
def apply_transform(value, transform, state_key:)
|
|
541
|
+
def apply_transform(value, transform, state_key:, frame:)
|
|
268
542
|
return value if transform.nil? || transform.empty?
|
|
269
543
|
return transform_array(value, transform) if value.is_a?(Array)
|
|
270
544
|
return nil if value.is_a?(Hash) || value.nil?
|
|
271
545
|
|
|
272
|
-
transformed = transform_scalar(value, transform)
|
|
546
|
+
transformed = transform_scalar(value, transform, state_key: state_key)
|
|
273
547
|
return nil if transformed.nil?
|
|
274
548
|
|
|
275
|
-
|
|
549
|
+
transformed = apply_trigger_mode(transformed, transform, state_key: state_key) if transform[:as] == :trigger
|
|
550
|
+
transformed = apply_event_shaping(transformed, transform, state_key: state_key, frame: frame)
|
|
551
|
+
return apply_smoothing(transformed, transform, state_key) unless transform[:as] == :trigger
|
|
552
|
+
transformed
|
|
276
553
|
end
|
|
277
554
|
|
|
278
555
|
def transform_array(value, transform)
|
|
@@ -281,11 +558,12 @@ module Vizcore
|
|
|
281
558
|
end
|
|
282
559
|
end
|
|
283
560
|
|
|
284
|
-
def transform_scalar(value, transform, fallback: nil)
|
|
561
|
+
def transform_scalar(value, transform, fallback: nil, state_key: nil)
|
|
285
562
|
numeric = numeric_value(value, fallback: fallback)
|
|
286
563
|
return nil if numeric.nil?
|
|
287
564
|
|
|
288
565
|
numeric = 0.0 if transform.key?(:deadzone) && numeric.abs < Float(transform[:deadzone])
|
|
566
|
+
numeric = apply_threshold(numeric, transform, state_key: state_key)
|
|
289
567
|
numeric *= Float(transform[:gain]) if transform.key?(:gain)
|
|
290
568
|
numeric = apply_curve(numeric, transform[:curve]) if transform[:curve]
|
|
291
569
|
numeric = [numeric, Float(transform[:min])].max if transform.key?(:min)
|
|
@@ -293,6 +571,20 @@ module Vizcore
|
|
|
293
571
|
numeric
|
|
294
572
|
end
|
|
295
573
|
|
|
574
|
+
def apply_threshold(value, transform, state_key:)
|
|
575
|
+
return value unless transform.key?(:threshold) || transform.key?(:hysteresis)
|
|
576
|
+
|
|
577
|
+
threshold = Float(transform.fetch(:threshold, 0.5))
|
|
578
|
+
hysteresis = Float(transform.fetch(:hysteresis, 0.0))
|
|
579
|
+
return value >= threshold ? value : 0.0 if hysteresis <= 0.0 || state_key.nil?
|
|
580
|
+
|
|
581
|
+
key = [:hysteresis, state_key]
|
|
582
|
+
active = !!@mapping_state[key]
|
|
583
|
+
active = value >= (active ? threshold - hysteresis : threshold)
|
|
584
|
+
@mapping_state[key] = active
|
|
585
|
+
active ? value : 0.0
|
|
586
|
+
end
|
|
587
|
+
|
|
296
588
|
def numeric_value(value, fallback:)
|
|
297
589
|
return value ? 1.0 : 0.0 if value == true || value == false
|
|
298
590
|
|
|
@@ -312,12 +604,111 @@ module Vizcore
|
|
|
312
604
|
when :ease_out
|
|
313
605
|
clamped = [[value, 0.0].max, 1.0].min
|
|
314
606
|
1.0 - ((1.0 - clamped) * (1.0 - clamped))
|
|
607
|
+
when :ease_in
|
|
608
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
609
|
+
clamped * clamped
|
|
610
|
+
when :ease_in_out
|
|
611
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
612
|
+
clamped < 0.5 ? 2.0 * clamped * clamped : 1.0 - ((-2.0 * clamped + 2.0)**2 / 2.0)
|
|
613
|
+
when :smoothstep
|
|
614
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
615
|
+
clamped * clamped * (3.0 - 2.0 * clamped)
|
|
616
|
+
when :exp
|
|
617
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
618
|
+
((Math.exp(clamped) - 1.0) / (Math::E - 1.0)).clamp(0.0, 1.0)
|
|
619
|
+
when :log
|
|
620
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
621
|
+
Math.log1p(clamped * (Math::E - 1.0))
|
|
622
|
+
when :step
|
|
623
|
+
value >= 0.5 ? 1.0 : 0.0
|
|
315
624
|
end
|
|
316
625
|
end
|
|
317
626
|
|
|
627
|
+
def apply_trigger_mode(value, _transform, state_key:)
|
|
628
|
+
key = [:trigger, state_key]
|
|
629
|
+
active = value.to_f > 0.0
|
|
630
|
+
previous = !!@mapping_state[key]
|
|
631
|
+
@mapping_state[key] = active
|
|
632
|
+
(active && !previous) ? 1.0 : 0.0
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def apply_event_shaping(value, transform, state_key:, frame:)
|
|
636
|
+
shaped = value
|
|
637
|
+
shaped = apply_cooldown(shaped, transform, state_key: state_key, frame: frame) if transform.key?(:cooldown)
|
|
638
|
+
shaped = apply_one_shot(shaped, transform, state_key: state_key) if transform[:one_shot]
|
|
639
|
+
shaped = apply_hold(shaped, transform, state_key: state_key, frame: frame) if transform.key?(:hold)
|
|
640
|
+
shaped = apply_decay(shaped, transform, state_key: state_key) if transform.key?(:decay)
|
|
641
|
+
shaped
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def apply_cooldown(value, transform, state_key:, frame:)
|
|
645
|
+
cooldown_frames = (Float(transform[:cooldown]) * 60.0).ceil
|
|
646
|
+
return value unless cooldown_frames.positive?
|
|
647
|
+
|
|
648
|
+
key = [:cooldown, state_key]
|
|
649
|
+
state = @mapping_state[key] || { until_frame: 0 }
|
|
650
|
+
current_frame = Integer(frame)
|
|
651
|
+
return value unless value.to_f > 0.0
|
|
652
|
+
|
|
653
|
+
if current_frame >= state[:until_frame]
|
|
654
|
+
state[:until_frame] = current_frame + cooldown_frames
|
|
655
|
+
@mapping_state[key] = state
|
|
656
|
+
value
|
|
657
|
+
else
|
|
658
|
+
0.0
|
|
659
|
+
end
|
|
660
|
+
rescue StandardError
|
|
661
|
+
value
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def apply_one_shot(value, _transform, state_key:)
|
|
665
|
+
key = [:one_shot, state_key]
|
|
666
|
+
|
|
667
|
+
active = value.to_f > 0.0
|
|
668
|
+
return 0.0 unless active
|
|
669
|
+
|
|
670
|
+
fired = !!@mapping_state[key]
|
|
671
|
+
return 0.0 if fired
|
|
672
|
+
|
|
673
|
+
@mapping_state[key] = true
|
|
674
|
+
value
|
|
675
|
+
rescue StandardError
|
|
676
|
+
value
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def apply_hold(value, transform, state_key:, frame:)
|
|
680
|
+
hold_frames = (Float(transform[:hold]) * 60.0).ceil
|
|
681
|
+
return value unless hold_frames.positive?
|
|
682
|
+
|
|
683
|
+
key = [:hold, state_key]
|
|
684
|
+
state = @mapping_state[key] || { until_frame: -1, value: 0.0 }
|
|
685
|
+
current_frame = Integer(frame)
|
|
686
|
+
if value.to_f.positive?
|
|
687
|
+
state = { until_frame: current_frame + hold_frames, value: value }
|
|
688
|
+
elsif current_frame <= state[:until_frame]
|
|
689
|
+
value = state[:value]
|
|
690
|
+
end
|
|
691
|
+
@mapping_state[key] = state
|
|
692
|
+
value
|
|
693
|
+
rescue StandardError
|
|
694
|
+
value
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def apply_decay(value, transform, state_key:)
|
|
698
|
+
decay = Float(transform[:decay]).clamp(0.0, 1.0)
|
|
699
|
+
key = [:decay, state_key]
|
|
700
|
+
previous = @mapping_state[key].to_f
|
|
701
|
+
output = [value.to_f, previous * decay].max
|
|
702
|
+
@mapping_state[key] = output
|
|
703
|
+
output
|
|
704
|
+
rescue StandardError
|
|
705
|
+
value
|
|
706
|
+
end
|
|
707
|
+
|
|
318
708
|
def apply_smoothing(value, transform, state_key)
|
|
319
709
|
return value unless transform.key?(:attack) || transform.key?(:release)
|
|
320
710
|
|
|
711
|
+
state_key = [:smooth, state_key]
|
|
321
712
|
previous = @mapping_state[state_key]
|
|
322
713
|
if previous.nil?
|
|
323
714
|
@mapping_state[state_key] = value
|
|
@@ -335,16 +726,7 @@ module Vizcore
|
|
|
335
726
|
end
|
|
336
727
|
|
|
337
728
|
def deep_dup(value)
|
|
338
|
-
|
|
339
|
-
when Hash
|
|
340
|
-
value.each_with_object({}) do |(key, entry), output|
|
|
341
|
-
output[key] = deep_dup(entry)
|
|
342
|
-
end
|
|
343
|
-
when Array
|
|
344
|
-
value.map { |entry| deep_dup(entry) }
|
|
345
|
-
else
|
|
346
|
-
value
|
|
347
|
-
end
|
|
729
|
+
Vizcore::DeepCopy.copy(value)
|
|
348
730
|
end
|
|
349
731
|
|
|
350
732
|
def deep_symbolize(value)
|
|
@@ -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
|