vizcore 0.1.0 → 1.1.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 +70 -117
- data/docs/.nojekyll +0 -0
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/assets/site.css +744 -0
- data/docs/assets/vizcore-demo.gif +0 -0
- data/docs/assets/vizcore-poster.png +0 -0
- data/docs/assets/vj-tunnel.js +159 -0
- data/docs/index.html +225 -0
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/examples/README.md +59 -0
- data/examples/assets/README.md +19 -0
- data/examples/audio_inspector.rb +34 -0
- data/examples/club_intro_drop.rb +78 -0
- data/examples/kansai_rubykaigi_visual.rb +70 -0
- data/examples/live_coding_minimal.rb +22 -0
- data/examples/midi_controller_show.rb +78 -0
- data/examples/midi_scene_switch.rb +3 -1
- data/examples/parser_visualizer.rb +48 -0
- data/examples/readme_demo.rb +17 -0
- data/examples/rhythm_geometry.rb +34 -0
- data/examples/ruby_crystal_show.rb +35 -0
- data/examples/shader_playground.rb +18 -0
- data/examples/unyo_liquid.rb +59 -0
- data/examples/vj_ambient_chill_room.rb +124 -0
- data/examples/vj_dnb_jungle.rb +170 -0
- data/examples/vj_festival_mainstage.rb +245 -0
- data/examples/vj_festival_mainstage.yml +17 -0
- data/examples/vj_glitch_industrial.rb +164 -0
- data/examples/vj_hiphop_cipher.rb +167 -0
- data/examples/vj_jpop_idol_live.rb +210 -0
- data/examples/vj_synthwave_retro.rb +173 -0
- data/examples/vj_techno_warehouse.rb +195 -0
- data/frontend/index.html +494 -2
- data/frontend/src/audio-inspector.js +40 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/live-controls.js +131 -0
- data/frontend/src/main.js +1060 -16
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/midi-learn.js +194 -0
- data/frontend/src/performance-monitor.js +183 -0
- data/frontend/src/plugin-runtime.js +130 -0
- data/frontend/src/projector-mode.js +56 -0
- data/frontend/src/renderer/engine.js +157 -3
- data/frontend/src/renderer/layer-manager.js +442 -30
- data/frontend/src/renderer/shader-manager.js +26 -0
- data/frontend/src/runtime-control-preset.js +11 -0
- data/frontend/src/shader-error-overlay.js +29 -0
- data/frontend/src/shader-param-controls.js +93 -0
- data/frontend/src/shaders/builtins.js +380 -2
- data/frontend/src/shaders/post-effects.js +52 -0
- data/frontend/src/shape-editor-controls.js +157 -0
- data/frontend/src/visual-regression.js +67 -0
- data/frontend/src/visual-settings-preset.js +103 -0
- data/frontend/src/visuals/geometry.js +666 -0
- data/frontend/src/visuals/image-renderer.js +291 -0
- data/frontend/src/visuals/particle-system.js +56 -10
- data/frontend/src/visuals/shape-renderer.js +475 -0
- data/frontend/src/visuals/spectrogram-renderer.js +226 -0
- data/frontend/src/visuals/svg-arc.js +104 -0
- data/frontend/src/visuals/text-renderer.js +112 -11
- data/frontend/src/websocket-client.js +12 -1
- data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
- data/lib/vizcore/analysis/beat_detector.rb +4 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
- data/lib/vizcore/analysis/feature_recorder.rb +159 -0
- data/lib/vizcore/analysis/feature_replay.rb +84 -0
- data/lib/vizcore/analysis/pipeline.rb +235 -11
- data/lib/vizcore/analysis/tap_tempo.rb +74 -0
- data/lib/vizcore/analysis.rb +4 -0
- data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
- data/lib/vizcore/audio/fixture_input.rb +65 -0
- data/lib/vizcore/audio/input_manager.rb +4 -2
- data/lib/vizcore/audio/mic_input.rb +24 -8
- data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/doctor.rb +159 -0
- data/lib/vizcore/cli/dsl_reference.rb +99 -0
- data/lib/vizcore/cli/layer_docs.rb +46 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
- data/lib/vizcore/cli/scene_inspector.rb +136 -0
- data/lib/vizcore/cli/scene_validator.rb +337 -0
- data/lib/vizcore/cli/shader_template.rb +68 -0
- data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
- data/lib/vizcore/cli.rb +689 -18
- data/lib/vizcore/config.rb +103 -2
- data/lib/vizcore/control_preset.rb +68 -0
- data/lib/vizcore/dsl/engine.rb +277 -5
- data/lib/vizcore/dsl/layer_builder.rb +1280 -23
- data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
- data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
- data/lib/vizcore/dsl/reaction_builder.rb +44 -0
- data/lib/vizcore/dsl/scene_builder.rb +61 -5
- data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
- data/lib/vizcore/dsl/style_builder.rb +68 -0
- data/lib/vizcore/dsl/timeline_builder.rb +138 -0
- data/lib/vizcore/dsl/transition_controller.rb +77 -0
- data/lib/vizcore/dsl.rb +5 -1
- data/lib/vizcore/layer_catalog.rb +275 -0
- data/lib/vizcore/project_manifest.rb +152 -0
- data/lib/vizcore/renderer/png_writer.rb +57 -0
- data/lib/vizcore/renderer/render_sequence.rb +153 -0
- data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
- data/lib/vizcore/renderer/scene_serializer.rb +36 -3
- data/lib/vizcore/renderer/snapshot.rb +38 -0
- data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
- data/lib/vizcore/renderer.rb +5 -0
- data/lib/vizcore/server/frame_broadcaster.rb +143 -8
- data/lib/vizcore/server/gallery_app.rb +155 -0
- data/lib/vizcore/server/gallery_page.rb +100 -0
- data/lib/vizcore/server/gallery_runner.rb +48 -0
- data/lib/vizcore/server/rack_app.rb +203 -4
- data/lib/vizcore/server/runner.rb +391 -22
- data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
- data/lib/vizcore/server/websocket_handler.rb +60 -10
- data/lib/vizcore/server.rb +4 -0
- data/lib/vizcore/shape.rb +719 -0
- data/lib/vizcore/sync/osc_message.rb +103 -0
- data/lib/vizcore/sync/osc_receiver.rb +68 -0
- data/lib/vizcore/sync.rb +4 -0
- data/lib/vizcore/templates/midi_control_scene.rb +3 -1
- data/lib/vizcore/templates/plugin_layer.rb +20 -0
- data/lib/vizcore/templates/plugin_readme.md +23 -0
- data/lib/vizcore/templates/plugin_renderer.js +43 -0
- data/lib/vizcore/templates/plugin_scene.rb +14 -0
- data/lib/vizcore/templates/project_readme.md +7 -23
- data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +28 -0
- data/scripts/browser_capture.mjs +75 -0
- data/sig/vizcore.rbs +461 -0
- metadata +94 -3
- data/docs/GETTING_STARTED.md +0 -105
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "layer_builder"
|
|
4
|
+
|
|
5
|
+
module Vizcore
|
|
6
|
+
module DSL
|
|
7
|
+
# Collects related layers and applies shared layer parameters.
|
|
8
|
+
class LayerGroupBuilder
|
|
9
|
+
# @param name [Symbol, String] group identifier stored on nested layer params
|
|
10
|
+
# @param styles [Hash] reusable layer parameter styles
|
|
11
|
+
# @param defaults [Hash] scene defaults already applied before group params
|
|
12
|
+
def initialize(name:, styles: {}, defaults: {})
|
|
13
|
+
@name = name.to_sym
|
|
14
|
+
@styles = styles
|
|
15
|
+
@params = deep_dup(defaults)
|
|
16
|
+
@layers = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Evaluate a group block.
|
|
20
|
+
#
|
|
21
|
+
# @yield Layer group DSL methods
|
|
22
|
+
# @return [Vizcore::DSL::LayerGroupBuilder]
|
|
23
|
+
def evaluate(&block)
|
|
24
|
+
instance_eval(&block) if block
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Define one layer in this group.
|
|
29
|
+
#
|
|
30
|
+
# @param name [Symbol, String] layer identifier
|
|
31
|
+
# @yield Layer definition block
|
|
32
|
+
# @return [void]
|
|
33
|
+
def layer(name, &block)
|
|
34
|
+
builder = LayerBuilder.new(name: name, styles: @styles, defaults: layer_defaults)
|
|
35
|
+
builder.evaluate(&block)
|
|
36
|
+
@layers << builder.to_h
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param value [Symbol, String] layer compositing mode shared by nested layers
|
|
40
|
+
# @return [Symbol]
|
|
41
|
+
def blend(value)
|
|
42
|
+
@params[:blend] = value.to_sym
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Store an ordered color palette shared by nested layers.
|
|
46
|
+
#
|
|
47
|
+
# @param colors [Array<String, Array<String>>] color values such as "#00ffff"
|
|
48
|
+
# @raise [ArgumentError] when no non-blank colors are supplied
|
|
49
|
+
# @return [Array<String>]
|
|
50
|
+
def palette(*colors)
|
|
51
|
+
@params[:palette] = normalize_palette(colors)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Merge a named style into this group's shared params.
|
|
55
|
+
#
|
|
56
|
+
# @param name [Symbol, String] style identifier
|
|
57
|
+
# @raise [ArgumentError] when the style is unknown
|
|
58
|
+
# @return [Hash] applied style params
|
|
59
|
+
def use_style(name)
|
|
60
|
+
style_name = name.to_sym
|
|
61
|
+
style_params = @styles.fetch(style_name) { raise ArgumentError, "unknown style: #{style_name}" }
|
|
62
|
+
@params.merge!(deep_dup(style_params))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Array<Hash>] serialized nested layers
|
|
66
|
+
def to_a
|
|
67
|
+
@layers.map { |layer| deep_dup(layer) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Stores dynamic one-argument setters into shared group params.
|
|
71
|
+
# @api private
|
|
72
|
+
def method_missing(method_name, *args, &block)
|
|
73
|
+
if block.nil? && args.length == 1
|
|
74
|
+
@params[method_name.to_sym] = args.first
|
|
75
|
+
return args.first
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
82
|
+
@params.key?(method_name.to_sym) || super
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def layer_defaults
|
|
88
|
+
deep_dup(@params).merge(group: @name)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def normalize_palette(colors)
|
|
92
|
+
values = colors.flatten.map { |color| color.to_s.strip }.reject(&:empty?)
|
|
93
|
+
raise ArgumentError, "group #{@name} palette requires at least one color" if values.empty?
|
|
94
|
+
|
|
95
|
+
values
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def deep_dup(value)
|
|
99
|
+
case value
|
|
100
|
+
when Hash
|
|
101
|
+
value.each_with_object({}) do |(key, entry), output|
|
|
102
|
+
output[key] = deep_dup(entry)
|
|
103
|
+
end
|
|
104
|
+
when Array
|
|
105
|
+
value.map { |entry| deep_dup(entry) }
|
|
106
|
+
else
|
|
107
|
+
value
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../shape"
|
|
4
|
+
|
|
3
5
|
module Vizcore
|
|
4
6
|
module DSL
|
|
5
7
|
# Resolves `map` definitions into concrete per-layer parameter values.
|
|
6
8
|
class MappingResolver
|
|
9
|
+
def initialize
|
|
10
|
+
@mapping_state = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
# @param scene_layers [Array<Hash>]
|
|
8
14
|
# @param audio [Hash]
|
|
9
15
|
# @return [Array<Hash>] normalized layer payloads with resolved params
|
|
10
|
-
def resolve_layers(scene_layers:, audio:)
|
|
16
|
+
def resolve_layers(scene_layers:, audio:, time: 0.0, frame: 0, resolution: [1280, 720], globals: {}, custom_shape_overrides: {})
|
|
11
17
|
normalize_scene_layers(scene_layers).map do |layer|
|
|
12
|
-
resolve_layer(layer, audio)
|
|
18
|
+
resolve_layer(layer, audio, time: time, frame: frame, resolution: resolution, globals: globals, custom_shape_overrides: custom_shape_overrides)
|
|
13
19
|
end
|
|
14
20
|
end
|
|
15
21
|
|
|
16
22
|
private
|
|
17
23
|
|
|
18
|
-
def resolve_layer(layer, audio)
|
|
19
|
-
params = (layer[:params] || {})
|
|
20
|
-
|
|
24
|
+
def resolve_layer(layer, audio, time:, frame:, resolution:, globals:, custom_shape_overrides:)
|
|
25
|
+
params = deep_dup(layer[:params] || {})
|
|
26
|
+
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]))
|
|
28
|
+
expand_dynamic_custom_shapes!(params, layer: layer, audio: audio, time: time, frame: frame, resolution: resolution, globals: globals)
|
|
21
29
|
|
|
22
30
|
output = {
|
|
23
31
|
name: layer.fetch(:name).to_s,
|
|
@@ -27,20 +35,201 @@ module Vizcore
|
|
|
27
35
|
output[:shader] = layer[:shader].to_s if layer[:shader]
|
|
28
36
|
output[:glsl] = layer[:glsl].to_s if layer[:glsl]
|
|
29
37
|
output[:glsl_source] = layer[:glsl_source].to_s if layer[:glsl_source]
|
|
38
|
+
output[:param_schema] = Array(layer[:param_schema]).map(&:dup) if layer[:param_schema]
|
|
30
39
|
output
|
|
31
40
|
end
|
|
32
41
|
|
|
33
|
-
def resolve_mappings(mappings, audio)
|
|
42
|
+
def resolve_mappings(mappings, audio, layer_name:)
|
|
34
43
|
Array(mappings).each_with_object({}) do |mapping, resolved|
|
|
35
44
|
source = mapping[:source]
|
|
36
45
|
target = mapping[:target]
|
|
37
46
|
next unless source && target
|
|
38
47
|
|
|
39
48
|
value = resolve_source_value(source, audio)
|
|
40
|
-
|
|
49
|
+
value = apply_transform(value, mapping[:transform], state_key: [layer_name, target, source])
|
|
50
|
+
resolved[target.to_s] = value unless value.nil?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def merge_resolved_mappings!(params, mappings)
|
|
55
|
+
mappings.each do |target, value|
|
|
56
|
+
if target.include?(".")
|
|
57
|
+
assign_nested_param(params, target.split("."), value)
|
|
58
|
+
else
|
|
59
|
+
params[target.to_sym] = value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def expand_dynamic_custom_shapes!(params, layer:, audio:, time:, frame:, resolution:, globals:)
|
|
65
|
+
descriptors = Array(params.delete(:custom_shapes) || params.delete("custom_shapes"))
|
|
66
|
+
return if descriptors.empty?
|
|
67
|
+
|
|
68
|
+
params[:shapes] = Array(params[:shapes])
|
|
69
|
+
controls = []
|
|
70
|
+
descriptors.each_with_index do |descriptor, index|
|
|
71
|
+
start_index = params[:shapes].length
|
|
72
|
+
expanded = expand_dynamic_custom_shape(descriptor, layer: layer, palette: params[:palette], audio: audio, time: time, frame: frame, resolution: resolution, globals: globals)
|
|
73
|
+
params[:shapes].concat(expanded)
|
|
74
|
+
controls << custom_shape_control_descriptor(descriptor, index: index, start_index: start_index, count: expanded.length)
|
|
75
|
+
end
|
|
76
|
+
params[:custom_shape_controls] = controls unless controls.empty?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def expand_dynamic_custom_shape(descriptor, layer:, palette:, audio:, time:, frame:, resolution:, globals:)
|
|
80
|
+
values = Hash(descriptor)
|
|
81
|
+
renderer = values.fetch(:renderer)
|
|
82
|
+
shape_name = values[:name] || renderer
|
|
83
|
+
primitives = Vizcore::Shape.expand_custom_shape(
|
|
84
|
+
renderer,
|
|
85
|
+
params: Hash(values[:params] || {}),
|
|
86
|
+
shape_id: values[:shape_id],
|
|
87
|
+
layer_name: layer[:name],
|
|
88
|
+
palette: Array(palette),
|
|
89
|
+
audio: audio,
|
|
90
|
+
time: time,
|
|
91
|
+
frame: frame,
|
|
92
|
+
resolution: resolution,
|
|
93
|
+
globals: globals,
|
|
94
|
+
shape_name: shape_name
|
|
95
|
+
)
|
|
96
|
+
primitives.each { |primitive| apply_custom_shape_attributes!(primitive, values) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def custom_shape_control_descriptor(descriptor, index:, start_index:, count:)
|
|
100
|
+
values = Hash(descriptor)
|
|
101
|
+
{
|
|
102
|
+
index: index,
|
|
103
|
+
name: (values[:name] || values["name"] || "custom_shape").to_s,
|
|
104
|
+
params: deep_dup(Hash(values[:params] || values["params"] || {})),
|
|
105
|
+
param_schema: Array(values[:param_schema] || values["param_schema"]).map { |entry| deep_dup(entry) },
|
|
106
|
+
shape_indices: (start_index...(start_index + count)).to_a
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def apply_custom_shape_overrides!(params, layer_name:, custom_shape_overrides:)
|
|
111
|
+
layer_overrides = custom_shape_layer_overrides(custom_shape_overrides, layer_name)
|
|
112
|
+
return if layer_overrides.empty?
|
|
113
|
+
|
|
114
|
+
descriptors = Array(params[:custom_shapes] || params["custom_shapes"])
|
|
115
|
+
layer_overrides.each do |index, values|
|
|
116
|
+
descriptor = descriptors[Integer(index)]
|
|
117
|
+
next unless descriptor && values.is_a?(Hash)
|
|
118
|
+
|
|
119
|
+
descriptor[:params] ||= {}
|
|
120
|
+
values.each do |param_name, value|
|
|
121
|
+
key = param_name.to_sym
|
|
122
|
+
descriptor[:params][key] = value
|
|
123
|
+
end
|
|
124
|
+
rescue ArgumentError, TypeError
|
|
125
|
+
next
|
|
41
126
|
end
|
|
42
127
|
end
|
|
43
128
|
|
|
129
|
+
def custom_shape_layer_overrides(overrides, layer_name)
|
|
130
|
+
values = Hash(overrides)
|
|
131
|
+
name = layer_name.to_s
|
|
132
|
+
Hash(values[name] || values[layer_name.to_sym] || {})
|
|
133
|
+
rescue TypeError
|
|
134
|
+
{}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def apply_custom_shape_attributes!(primitive, descriptor)
|
|
138
|
+
style = Hash(descriptor[:style] || {})
|
|
139
|
+
style.each do |key, value|
|
|
140
|
+
symbol_key = key.to_sym
|
|
141
|
+
if symbol_key == :opacity && primitive.key?(:opacity)
|
|
142
|
+
primitive[:opacity] = numeric(style[:opacity] || style["opacity"], :opacity) * numeric(primitive[:opacity], :opacity)
|
|
143
|
+
else
|
|
144
|
+
primitive[symbol_key] = deep_dup(value) unless primitive.key?(symbol_key)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
transform = Hash(descriptor[:transform] || {})
|
|
149
|
+
primitive[:transform] = compose_shape_transform(transform, primitive[:transform]) unless transform.empty?
|
|
150
|
+
primitive
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def assign_nested_param(container, path, value)
|
|
154
|
+
key = path.shift
|
|
155
|
+
if path.empty?
|
|
156
|
+
assign_nested_value(container, key, value)
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
next_container = nested_value(container, key)
|
|
161
|
+
next_container = create_nested_container(container, key, path.first) if next_container.nil?
|
|
162
|
+
return unless next_container
|
|
163
|
+
|
|
164
|
+
assign_nested_param(next_container, path, value)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def nested_value(container, key)
|
|
168
|
+
return container[key.to_i] if container.is_a?(Array) && integer_key?(key)
|
|
169
|
+
return container[key.to_sym] if container.is_a?(Hash)
|
|
170
|
+
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def create_nested_container(container, key, next_key)
|
|
175
|
+
return unless container.is_a?(Hash)
|
|
176
|
+
|
|
177
|
+
value = integer_key?(next_key) ? [] : {}
|
|
178
|
+
container[key.to_sym] = value
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def assign_nested_value(container, key, value)
|
|
182
|
+
if container.is_a?(Array) && integer_key?(key)
|
|
183
|
+
container[key.to_i] = value
|
|
184
|
+
elsif container.is_a?(Hash)
|
|
185
|
+
container[key.to_sym] = value
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def integer_key?(value)
|
|
190
|
+
value.match?(/\A\d+\z/)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def compose_shape_transform(parent, child)
|
|
194
|
+
return deep_dup(child || {}) unless parent
|
|
195
|
+
|
|
196
|
+
child ||= {}
|
|
197
|
+
output = deep_dup(parent)
|
|
198
|
+
output[:translate] = add_shape_xy(parent[:translate], child[:translate]) if child.key?(:translate)
|
|
199
|
+
output[:origin] = child[:origin] if child.key?(:origin)
|
|
200
|
+
output[:rotate] = numeric(parent[:rotate] || 0, :rotate) + numeric(child[:rotate] || 0, :rotate) if child.key?(:rotate)
|
|
201
|
+
output[:scale] = multiply_shape_scale(parent[:scale], child[:scale]) if child.key?(:scale)
|
|
202
|
+
output
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def add_shape_xy(parent, child)
|
|
206
|
+
parent ||= {}
|
|
207
|
+
child ||= {}
|
|
208
|
+
{
|
|
209
|
+
x: numeric(parent[:x] || parent["x"] || 0, :"translate.x") + numeric(child[:x] || child["x"] || 0, :"translate.x"),
|
|
210
|
+
y: numeric(parent[:y] || parent["y"] || 0, :"translate.y") + numeric(child[:y] || child["y"] || 0, :"translate.y")
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def multiply_shape_scale(parent, child)
|
|
215
|
+
parent = shape_scale_pair(parent)
|
|
216
|
+
child = shape_scale_pair(child)
|
|
217
|
+
{ x: parent[:x] * child[:x], y: parent[:y] * child[:y] }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def shape_scale_pair(value)
|
|
221
|
+
return { x: numeric(value[:x] || value["x"] || 1, :"scale.x"), y: numeric(value[:y] || value["y"] || 1, :"scale.y") } if value.is_a?(Hash)
|
|
222
|
+
|
|
223
|
+
scale = numeric(value || 1, :scale)
|
|
224
|
+
{ x: scale, y: scale }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def numeric(value, name)
|
|
228
|
+
Float(value)
|
|
229
|
+
rescue ArgumentError, TypeError
|
|
230
|
+
raise ArgumentError, "param #{name} must be numeric"
|
|
231
|
+
end
|
|
232
|
+
|
|
44
233
|
def resolve_source_value(source, audio)
|
|
45
234
|
case source[:kind]&.to_sym
|
|
46
235
|
when :amplitude
|
|
@@ -49,8 +238,16 @@ module Vizcore
|
|
|
49
238
|
audio.dig(:bands, source[:band]&.to_sym)
|
|
50
239
|
when :fft_spectrum
|
|
51
240
|
audio[:fft]
|
|
241
|
+
when :onset
|
|
242
|
+
resolve_onset(source, audio)
|
|
243
|
+
when :kick, :snare, :hihat
|
|
244
|
+
audio.dig(:drums, source[:kind].to_sym)
|
|
52
245
|
when :beat
|
|
53
246
|
audio[:beat]
|
|
247
|
+
when :beat_confidence
|
|
248
|
+
audio[:beat_confidence]
|
|
249
|
+
when :beat_pulse
|
|
250
|
+
audio[:beat_pulse]
|
|
54
251
|
when :beat_count
|
|
55
252
|
audio[:beat_count]
|
|
56
253
|
when :bpm
|
|
@@ -60,10 +257,96 @@ module Vizcore
|
|
|
60
257
|
end
|
|
61
258
|
end
|
|
62
259
|
|
|
260
|
+
def resolve_onset(source, audio)
|
|
261
|
+
band = source[:band]&.to_sym
|
|
262
|
+
return audio[:onset] unless band
|
|
263
|
+
|
|
264
|
+
audio.dig(:onsets, band)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def apply_transform(value, transform, state_key:)
|
|
268
|
+
return value if transform.nil? || transform.empty?
|
|
269
|
+
return transform_array(value, transform) if value.is_a?(Array)
|
|
270
|
+
return nil if value.is_a?(Hash) || value.nil?
|
|
271
|
+
|
|
272
|
+
transformed = transform_scalar(value, transform)
|
|
273
|
+
return nil if transformed.nil?
|
|
274
|
+
|
|
275
|
+
apply_smoothing(transformed, transform, state_key)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def transform_array(value, transform)
|
|
279
|
+
value.map do |entry|
|
|
280
|
+
transform_scalar(entry, transform, fallback: 0.0) || 0.0
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def transform_scalar(value, transform, fallback: nil)
|
|
285
|
+
numeric = numeric_value(value, fallback: fallback)
|
|
286
|
+
return nil if numeric.nil?
|
|
287
|
+
|
|
288
|
+
numeric = 0.0 if transform.key?(:deadzone) && numeric.abs < Float(transform[:deadzone])
|
|
289
|
+
numeric *= Float(transform[:gain]) if transform.key?(:gain)
|
|
290
|
+
numeric = apply_curve(numeric, transform[:curve]) if transform[:curve]
|
|
291
|
+
numeric = [numeric, Float(transform[:min])].max if transform.key?(:min)
|
|
292
|
+
numeric = [numeric, Float(transform[:max])].min if transform.key?(:max)
|
|
293
|
+
numeric
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def numeric_value(value, fallback:)
|
|
297
|
+
return value ? 1.0 : 0.0 if value == true || value == false
|
|
298
|
+
|
|
299
|
+
Float(value)
|
|
300
|
+
rescue ArgumentError, TypeError
|
|
301
|
+
fallback
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def apply_curve(value, curve)
|
|
305
|
+
case curve.to_sym
|
|
306
|
+
when :linear
|
|
307
|
+
value
|
|
308
|
+
when :sqrt
|
|
309
|
+
Math.sqrt([value, 0.0].max)
|
|
310
|
+
when :square
|
|
311
|
+
value * value
|
|
312
|
+
when :ease_out
|
|
313
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
314
|
+
1.0 - ((1.0 - clamped) * (1.0 - clamped))
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def apply_smoothing(value, transform, state_key)
|
|
319
|
+
return value unless transform.key?(:attack) || transform.key?(:release)
|
|
320
|
+
|
|
321
|
+
previous = @mapping_state[state_key]
|
|
322
|
+
if previous.nil?
|
|
323
|
+
@mapping_state[state_key] = value
|
|
324
|
+
return value
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
alpha = value >= previous ? transform.fetch(:attack, 1.0) : transform.fetch(:release, 1.0)
|
|
328
|
+
smoothed = previous + (value - previous) * alpha
|
|
329
|
+
@mapping_state[state_key] = smoothed
|
|
330
|
+
smoothed
|
|
331
|
+
end
|
|
332
|
+
|
|
63
333
|
def normalize_scene_layers(scene_layers)
|
|
64
334
|
Array(scene_layers).map { |layer| deep_symbolize(layer) }
|
|
65
335
|
end
|
|
66
336
|
|
|
337
|
+
def deep_dup(value)
|
|
338
|
+
case value
|
|
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
|
|
348
|
+
end
|
|
349
|
+
|
|
67
350
|
def deep_symbolize(value)
|
|
68
351
|
case value
|
|
69
352
|
when Hash
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vizcore
|
|
4
|
+
module DSL
|
|
5
|
+
# Collects block-style mapping transform options.
|
|
6
|
+
class MappingTransformBuilder
|
|
7
|
+
# @param initial [Hash]
|
|
8
|
+
def initialize(initial = {})
|
|
9
|
+
@values = initial.each_with_object({}) do |(key, value), output|
|
|
10
|
+
output[key.to_sym] = value
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [Vizcore::DSL::MappingTransformBuilder]
|
|
15
|
+
def evaluate(&block)
|
|
16
|
+
instance_eval(&block) if block
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param value [Numeric]
|
|
21
|
+
# @return [Numeric]
|
|
22
|
+
def gain(value)
|
|
23
|
+
@values[:gain] = value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param value [Range, Array]
|
|
27
|
+
# @return [Range, Array]
|
|
28
|
+
def range(value)
|
|
29
|
+
@values[:range] = value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param value [Numeric]
|
|
33
|
+
# @return [Numeric]
|
|
34
|
+
def min(value)
|
|
35
|
+
@values[:min] = value
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param value [Numeric]
|
|
39
|
+
# @return [Numeric]
|
|
40
|
+
def max(value)
|
|
41
|
+
@values[:max] = value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param value [Symbol, String]
|
|
45
|
+
# @return [Symbol, String]
|
|
46
|
+
def curve(value)
|
|
47
|
+
@values[:curve] = value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param value [Numeric]
|
|
51
|
+
# @return [Numeric]
|
|
52
|
+
def deadzone(value)
|
|
53
|
+
@values[:deadzone] = value
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param attack [Numeric, nil]
|
|
57
|
+
# @param release [Numeric, nil]
|
|
58
|
+
# @return [Hash]
|
|
59
|
+
def smooth(attack: nil, release: nil)
|
|
60
|
+
@values[:attack] = attack unless attack.nil?
|
|
61
|
+
@values[:release] = release unless release.nil?
|
|
62
|
+
@values
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Hash]
|
|
66
|
+
def to_h
|
|
67
|
+
@values.dup
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vizcore
|
|
4
|
+
module DSL
|
|
5
|
+
# Collects high-level `react_to` DSL entries and converts them to mappings.
|
|
6
|
+
class ReactionBuilder
|
|
7
|
+
# @param mapping_factory [#call] builds one normalized mapping hash
|
|
8
|
+
def initialize(mapping_factory:)
|
|
9
|
+
@mapping_factory = mapping_factory
|
|
10
|
+
@mappings = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Evaluate a `react_to` block.
|
|
14
|
+
#
|
|
15
|
+
# @yield Reaction DSL methods
|
|
16
|
+
# @raise [ArgumentError] when the block does not define any reaction
|
|
17
|
+
# @return [Array<Hash>] normalized mapping payloads
|
|
18
|
+
def evaluate(&block)
|
|
19
|
+
instance_eval(&block) if block
|
|
20
|
+
raise ArgumentError, "react_to requires at least one change or trigger" if @mappings.empty?
|
|
21
|
+
|
|
22
|
+
@mappings.map(&:dup)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Continuously map the reaction source to a target parameter.
|
|
26
|
+
#
|
|
27
|
+
# @param target [Symbol, String] layer parameter name
|
|
28
|
+
# @param options [Hash] mapping transform options
|
|
29
|
+
# @return [void]
|
|
30
|
+
def change(target, **options)
|
|
31
|
+
@mappings << @mapping_factory.call(target, options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Map the reaction source to an event-like target parameter.
|
|
35
|
+
#
|
|
36
|
+
# @param target [Symbol, String] layer parameter name
|
|
37
|
+
# @param options [Hash] mapping transform options
|
|
38
|
+
# @return [void]
|
|
39
|
+
def trigger(target, **options)
|
|
40
|
+
change(target, **options)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "layer_builder"
|
|
4
|
+
require_relative "layer_group_builder"
|
|
4
5
|
|
|
5
6
|
module Vizcore
|
|
6
7
|
module DSL
|
|
7
8
|
# Collects layer definitions inside a single scene block.
|
|
8
9
|
class SceneBuilder
|
|
9
10
|
# @param name [Symbol, String] scene identifier
|
|
10
|
-
|
|
11
|
+
# @param styles [Hash] reusable layer parameter styles
|
|
12
|
+
# @param themes [Hash] reusable scene-wide layer parameter themes
|
|
13
|
+
# @param layers [Array<Hash>] initial layer definitions
|
|
14
|
+
def initialize(name:, styles: {}, themes: {}, layers: [])
|
|
11
15
|
@name = name.to_sym
|
|
12
|
-
@
|
|
16
|
+
@styles = styles
|
|
17
|
+
@themes = themes
|
|
18
|
+
@theme_name = nil
|
|
19
|
+
@theme_params = {}
|
|
20
|
+
@layers = layers.map { |layer| deep_dup(layer) }
|
|
13
21
|
end
|
|
14
22
|
|
|
15
23
|
# Evaluate a scene block.
|
|
@@ -27,17 +35,65 @@ module Vizcore
|
|
|
27
35
|
# @yield Layer definition block
|
|
28
36
|
# @return [void]
|
|
29
37
|
def layer(name, &block)
|
|
30
|
-
builder = LayerBuilder.new(name: name)
|
|
38
|
+
builder = LayerBuilder.new(name: name, styles: @styles, defaults: @theme_params)
|
|
31
39
|
builder.evaluate(&block)
|
|
32
40
|
@layers << builder.to_h
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
# Define a related group of layers with shared params.
|
|
44
|
+
#
|
|
45
|
+
# @param name [Symbol, String] group identifier
|
|
46
|
+
# @yield Layer group definition block
|
|
47
|
+
# @return [void]
|
|
48
|
+
def group(name, &block)
|
|
49
|
+
builder = LayerGroupBuilder.new(name: name, styles: @styles, defaults: @theme_params)
|
|
50
|
+
builder.evaluate(&block)
|
|
51
|
+
@layers.concat(builder.to_a)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Apply a named theme as default params for all layers in this scene.
|
|
55
|
+
#
|
|
56
|
+
# @param name [Symbol, String] theme identifier
|
|
57
|
+
# @raise [ArgumentError] when the theme is unknown
|
|
58
|
+
# @return [Hash] applied theme params
|
|
59
|
+
def use_theme(name)
|
|
60
|
+
theme_name = name.to_sym
|
|
61
|
+
theme_params = @themes.fetch(theme_name) { raise ArgumentError, "unknown theme: #{theme_name}" }
|
|
62
|
+
@theme_name = theme_name
|
|
63
|
+
@theme_params = deep_dup(theme_params)
|
|
64
|
+
@layers = @layers.map { |layer| apply_theme_defaults(layer, @theme_params) }
|
|
65
|
+
deep_dup(@theme_params)
|
|
66
|
+
end
|
|
67
|
+
|
|
35
68
|
# @return [Hash] serialized scene payload
|
|
36
69
|
def to_h
|
|
37
|
-
{
|
|
70
|
+
scene = {
|
|
38
71
|
name: @name,
|
|
39
|
-
layers: @layers.map { |layer| layer
|
|
72
|
+
layers: @layers.map { |layer| deep_dup(layer) }
|
|
40
73
|
}
|
|
74
|
+
scene[:theme] = @theme_name if @theme_name
|
|
75
|
+
scene
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def apply_theme_defaults(layer, theme_params)
|
|
81
|
+
themed_layer = deep_dup(layer)
|
|
82
|
+
themed_layer[:params] = deep_dup(theme_params).merge(Hash(themed_layer[:params] || {}))
|
|
83
|
+
themed_layer
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def deep_dup(value)
|
|
87
|
+
case value
|
|
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
|
|
41
97
|
end
|
|
42
98
|
end
|
|
43
99
|
end
|