vizcore 0.1.0 → 1.0.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 +544 -9
- data/docs/.nojekyll +0 -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 +224 -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 +468 -2
- data/frontend/src/audio-inspector.js +40 -0
- data/frontend/src/live-controls.js +131 -0
- data/frontend/src/main.js +792 -16
- 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 +148 -3
- data/frontend/src/renderer/layer-manager.js +428 -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/visual-regression.js +67 -0
- data/frontend/src/visual-settings-preset.js +103 -0
- data/frontend/src/visuals/geometry.js +268 -0
- data/frontend/src/visuals/image-renderer.js +291 -0
- data/frontend/src/visuals/particle-system.js +56 -10
- data/frontend/src/visuals/spectrogram-renderer.js +226 -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 +245 -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 +491 -22
- data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +132 -3
- 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 +273 -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 +119 -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 +446 -0
- data/lib/vizcore/renderer.rb +5 -0
- data/lib/vizcore/server/frame_broadcaster.rb +91 -5
- 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 +370 -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/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 +27 -0
- data/scripts/browser_capture.mjs +75 -0
- data/sig/vizcore.rbs +362 -0
- metadata +83 -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
|
|
@@ -4,6 +4,10 @@ module Vizcore
|
|
|
4
4
|
module DSL
|
|
5
5
|
# Resolves `map` definitions into concrete per-layer parameter values.
|
|
6
6
|
class MappingResolver
|
|
7
|
+
def initialize
|
|
8
|
+
@mapping_state = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
# @param scene_layers [Array<Hash>]
|
|
8
12
|
# @param audio [Hash]
|
|
9
13
|
# @return [Array<Hash>] normalized layer payloads with resolved params
|
|
@@ -17,7 +21,7 @@ module Vizcore
|
|
|
17
21
|
|
|
18
22
|
def resolve_layer(layer, audio)
|
|
19
23
|
params = (layer[:params] || {}).dup
|
|
20
|
-
|
|
24
|
+
merge_resolved_mappings!(params, resolve_mappings(layer[:mappings], audio, layer_name: layer[:name]))
|
|
21
25
|
|
|
22
26
|
output = {
|
|
23
27
|
name: layer.fetch(:name).to_s,
|
|
@@ -27,20 +31,64 @@ module Vizcore
|
|
|
27
31
|
output[:shader] = layer[:shader].to_s if layer[:shader]
|
|
28
32
|
output[:glsl] = layer[:glsl].to_s if layer[:glsl]
|
|
29
33
|
output[:glsl_source] = layer[:glsl_source].to_s if layer[:glsl_source]
|
|
34
|
+
output[:param_schema] = Array(layer[:param_schema]).map(&:dup) if layer[:param_schema]
|
|
30
35
|
output
|
|
31
36
|
end
|
|
32
37
|
|
|
33
|
-
def resolve_mappings(mappings, audio)
|
|
38
|
+
def resolve_mappings(mappings, audio, layer_name:)
|
|
34
39
|
Array(mappings).each_with_object({}) do |mapping, resolved|
|
|
35
40
|
source = mapping[:source]
|
|
36
41
|
target = mapping[:target]
|
|
37
42
|
next unless source && target
|
|
38
43
|
|
|
39
44
|
value = resolve_source_value(source, audio)
|
|
40
|
-
|
|
45
|
+
value = apply_transform(value, mapping[:transform], state_key: [layer_name, target, source])
|
|
46
|
+
resolved[target.to_s] = value unless value.nil?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def merge_resolved_mappings!(params, mappings)
|
|
51
|
+
mappings.each do |target, value|
|
|
52
|
+
if target.include?(".")
|
|
53
|
+
assign_nested_param(params, target.split("."), value)
|
|
54
|
+
else
|
|
55
|
+
params[target.to_sym] = value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def assign_nested_param(container, path, value)
|
|
61
|
+
key = path.shift
|
|
62
|
+
if path.empty?
|
|
63
|
+
assign_nested_value(container, key, value)
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
next_container = nested_value(container, key)
|
|
68
|
+
return unless next_container
|
|
69
|
+
|
|
70
|
+
assign_nested_param(next_container, path, value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def nested_value(container, key)
|
|
74
|
+
return container[key.to_i] if container.is_a?(Array) && integer_key?(key)
|
|
75
|
+
return container[key.to_sym] if container.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def assign_nested_value(container, key, value)
|
|
81
|
+
if container.is_a?(Array) && integer_key?(key)
|
|
82
|
+
container[key.to_i] = value
|
|
83
|
+
elsif container.is_a?(Hash)
|
|
84
|
+
container[key.to_sym] = value
|
|
41
85
|
end
|
|
42
86
|
end
|
|
43
87
|
|
|
88
|
+
def integer_key?(value)
|
|
89
|
+
value.match?(/\A\d+\z/)
|
|
90
|
+
end
|
|
91
|
+
|
|
44
92
|
def resolve_source_value(source, audio)
|
|
45
93
|
case source[:kind]&.to_sym
|
|
46
94
|
when :amplitude
|
|
@@ -49,8 +97,16 @@ module Vizcore
|
|
|
49
97
|
audio.dig(:bands, source[:band]&.to_sym)
|
|
50
98
|
when :fft_spectrum
|
|
51
99
|
audio[:fft]
|
|
100
|
+
when :onset
|
|
101
|
+
resolve_onset(source, audio)
|
|
102
|
+
when :kick, :snare, :hihat
|
|
103
|
+
audio.dig(:drums, source[:kind].to_sym)
|
|
52
104
|
when :beat
|
|
53
105
|
audio[:beat]
|
|
106
|
+
when :beat_confidence
|
|
107
|
+
audio[:beat_confidence]
|
|
108
|
+
when :beat_pulse
|
|
109
|
+
audio[:beat_pulse]
|
|
54
110
|
when :beat_count
|
|
55
111
|
audio[:beat_count]
|
|
56
112
|
when :bpm
|
|
@@ -60,6 +116,79 @@ module Vizcore
|
|
|
60
116
|
end
|
|
61
117
|
end
|
|
62
118
|
|
|
119
|
+
def resolve_onset(source, audio)
|
|
120
|
+
band = source[:band]&.to_sym
|
|
121
|
+
return audio[:onset] unless band
|
|
122
|
+
|
|
123
|
+
audio.dig(:onsets, band)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def apply_transform(value, transform, state_key:)
|
|
127
|
+
return value if transform.nil? || transform.empty?
|
|
128
|
+
return transform_array(value, transform) if value.is_a?(Array)
|
|
129
|
+
return nil if value.is_a?(Hash) || value.nil?
|
|
130
|
+
|
|
131
|
+
transformed = transform_scalar(value, transform)
|
|
132
|
+
return nil if transformed.nil?
|
|
133
|
+
|
|
134
|
+
apply_smoothing(transformed, transform, state_key)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def transform_array(value, transform)
|
|
138
|
+
value.map do |entry|
|
|
139
|
+
transform_scalar(entry, transform, fallback: 0.0) || 0.0
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def transform_scalar(value, transform, fallback: nil)
|
|
144
|
+
numeric = numeric_value(value, fallback: fallback)
|
|
145
|
+
return nil if numeric.nil?
|
|
146
|
+
|
|
147
|
+
numeric = 0.0 if transform.key?(:deadzone) && numeric.abs < Float(transform[:deadzone])
|
|
148
|
+
numeric *= Float(transform[:gain]) if transform.key?(:gain)
|
|
149
|
+
numeric = apply_curve(numeric, transform[:curve]) if transform[:curve]
|
|
150
|
+
numeric = [numeric, Float(transform[:min])].max if transform.key?(:min)
|
|
151
|
+
numeric = [numeric, Float(transform[:max])].min if transform.key?(:max)
|
|
152
|
+
numeric
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def numeric_value(value, fallback:)
|
|
156
|
+
return value ? 1.0 : 0.0 if value == true || value == false
|
|
157
|
+
|
|
158
|
+
Float(value)
|
|
159
|
+
rescue ArgumentError, TypeError
|
|
160
|
+
fallback
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def apply_curve(value, curve)
|
|
164
|
+
case curve.to_sym
|
|
165
|
+
when :linear
|
|
166
|
+
value
|
|
167
|
+
when :sqrt
|
|
168
|
+
Math.sqrt([value, 0.0].max)
|
|
169
|
+
when :square
|
|
170
|
+
value * value
|
|
171
|
+
when :ease_out
|
|
172
|
+
clamped = [[value, 0.0].max, 1.0].min
|
|
173
|
+
1.0 - ((1.0 - clamped) * (1.0 - clamped))
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def apply_smoothing(value, transform, state_key)
|
|
178
|
+
return value unless transform.key?(:attack) || transform.key?(:release)
|
|
179
|
+
|
|
180
|
+
previous = @mapping_state[state_key]
|
|
181
|
+
if previous.nil?
|
|
182
|
+
@mapping_state[state_key] = value
|
|
183
|
+
return value
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
alpha = value >= previous ? transform.fetch(:attack, 1.0) : transform.fetch(:release, 1.0)
|
|
187
|
+
smoothed = previous + (value - previous) * alpha
|
|
188
|
+
@mapping_state[state_key] = smoothed
|
|
189
|
+
smoothed
|
|
190
|
+
end
|
|
191
|
+
|
|
63
192
|
def normalize_scene_layers(scene_layers)
|
|
64
193
|
Array(scene_layers).map { |layer| deep_symbolize(layer) }
|
|
65
194
|
end
|
|
@@ -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
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "base64"
|
|
3
4
|
require "pathname"
|
|
4
5
|
|
|
5
6
|
module Vizcore
|
|
6
7
|
module DSL
|
|
7
|
-
# Replaces layer
|
|
8
|
+
# Replaces external layer source paths with browser-ready inline payloads.
|
|
8
9
|
class ShaderSourceResolver
|
|
10
|
+
MEDIA_MIME_TYPES = {
|
|
11
|
+
".gif" => "image/gif",
|
|
12
|
+
".jpg" => "image/jpeg",
|
|
13
|
+
".jpeg" => "image/jpeg",
|
|
14
|
+
".png" => "image/png",
|
|
15
|
+
".svg" => "image/svg+xml",
|
|
16
|
+
".webp" => "image/webp",
|
|
17
|
+
".mp4" => "video/mp4",
|
|
18
|
+
".ogv" => "video/ogg",
|
|
19
|
+
".webm" => "video/webm"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
9
22
|
# @param definition [Hash] DSL definition payload
|
|
10
23
|
# @param scene_file [String, Pathname] source scene file
|
|
11
|
-
# @raise [ArgumentError] when a referenced
|
|
12
|
-
# @return [Hash] deep-copied definition with
|
|
24
|
+
# @raise [ArgumentError] when a referenced source file is missing
|
|
25
|
+
# @return [Hash] deep-copied definition with resolved layer source entries
|
|
13
26
|
def resolve(definition:, scene_file:)
|
|
14
27
|
scene_path = Pathname.new(scene_file.to_s).expand_path
|
|
15
28
|
base_dir = scene_path.dirname
|
|
@@ -29,8 +42,13 @@ module Vizcore
|
|
|
29
42
|
def resolve_layer(layer, base_dir:)
|
|
30
43
|
layer_hash = symbolize_hash(layer)
|
|
31
44
|
shader_path = layer_hash[:glsl]
|
|
32
|
-
|
|
45
|
+
layer_hash = resolve_shader_layer(layer_hash, base_dir: base_dir) if shader_path
|
|
46
|
+
layer_hash = resolve_media_layer(layer_hash, base_dir: base_dir) if media_layer?(layer_hash)
|
|
47
|
+
layer_hash
|
|
48
|
+
end
|
|
33
49
|
|
|
50
|
+
def resolve_shader_layer(layer_hash, base_dir:)
|
|
51
|
+
shader_path = layer_hash.fetch(:glsl)
|
|
34
52
|
full_path = resolve_path(base_dir: base_dir, shader_path: shader_path)
|
|
35
53
|
raise ArgumentError, "GLSL file not found: #{shader_path}" unless full_path.file?
|
|
36
54
|
|
|
@@ -39,8 +57,51 @@ module Vizcore
|
|
|
39
57
|
layer_hash
|
|
40
58
|
end
|
|
41
59
|
|
|
42
|
-
def
|
|
43
|
-
|
|
60
|
+
def resolve_media_layer(layer_hash, base_dir:)
|
|
61
|
+
params = symbolize_hash(layer_hash[:params] || {})
|
|
62
|
+
media_path = params[:file]
|
|
63
|
+
return layer_hash unless media_path
|
|
64
|
+
|
|
65
|
+
full_path = resolve_path(base_dir: base_dir, relative_path: media_path)
|
|
66
|
+
raise ArgumentError, "#{media_layer_label(layer_hash)} file not found: #{media_path}" unless full_path.file?
|
|
67
|
+
|
|
68
|
+
mime_type = MEDIA_MIME_TYPES[full_path.extname.downcase]
|
|
69
|
+
raise ArgumentError, "Unsupported #{media_layer_label(layer_hash)} file extension: #{media_path}" unless mime_type
|
|
70
|
+
raise ArgumentError, "Unsupported SVG file extension: #{media_path}" if svg_layer?(layer_hash) && mime_type != "image/svg+xml"
|
|
71
|
+
raise ArgumentError, "Unsupported Image file extension: #{media_path}" if image_layer?(layer_hash) && !mime_type.start_with?("image/")
|
|
72
|
+
raise ArgumentError, "Unsupported Video file extension: #{media_path}" if video_layer?(layer_hash) && !mime_type.start_with?("video/")
|
|
73
|
+
|
|
74
|
+
params[:file] = media_path.to_s
|
|
75
|
+
params[:src] = "data:#{mime_type};base64,#{Base64.strict_encode64(full_path.binread)}"
|
|
76
|
+
layer_hash[:params] = params
|
|
77
|
+
layer_hash
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def svg_layer?(layer_hash)
|
|
81
|
+
%i[svg svg_layer].include?(layer_hash[:type]&.to_sym)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def image_layer?(layer_hash)
|
|
85
|
+
%i[image image_layer photo].include?(layer_hash[:type]&.to_sym)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def video_layer?(layer_hash)
|
|
89
|
+
%i[video video_layer footage].include?(layer_hash[:type]&.to_sym)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def media_layer?(layer_hash)
|
|
93
|
+
svg_layer?(layer_hash) || image_layer?(layer_hash) || video_layer?(layer_hash)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def media_layer_label(layer_hash)
|
|
97
|
+
return "SVG" if svg_layer?(layer_hash)
|
|
98
|
+
return "Video" if video_layer?(layer_hash)
|
|
99
|
+
|
|
100
|
+
"Image"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def resolve_path(base_dir:, relative_path: nil, shader_path: nil)
|
|
104
|
+
path = Pathname.new((relative_path || shader_path).to_s)
|
|
44
105
|
return path.expand_path if path.absolute?
|
|
45
106
|
|
|
46
107
|
base_dir.join(path).expand_path
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vizcore
|
|
4
|
+
module DSL
|
|
5
|
+
# Collects reusable layer parameter presets for the `style` DSL.
|
|
6
|
+
class StyleBuilder
|
|
7
|
+
# @param name [Symbol, String] style identifier
|
|
8
|
+
# @param kind [String] user-facing DSL kind for error messages
|
|
9
|
+
def initialize(name:, kind: "style")
|
|
10
|
+
@name = name.to_sym
|
|
11
|
+
@kind = kind
|
|
12
|
+
@params = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Evaluate a style block.
|
|
16
|
+
#
|
|
17
|
+
# @yield Style parameter declarations
|
|
18
|
+
# @return [Vizcore::DSL::StyleBuilder]
|
|
19
|
+
def evaluate(&block)
|
|
20
|
+
instance_eval(&block) if block
|
|
21
|
+
raise ArgumentError, "#{@kind} #{@name} requires at least one parameter" if @params.empty?
|
|
22
|
+
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Hash] serialized style payload
|
|
27
|
+
def to_h
|
|
28
|
+
{
|
|
29
|
+
name: @name,
|
|
30
|
+
params: @params.dup
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Store an ordered color palette for styles and themes.
|
|
35
|
+
#
|
|
36
|
+
# @param colors [Array<String, Array<String>>] color values such as "#00ffff"
|
|
37
|
+
# @raise [ArgumentError] when no non-blank colors are supplied
|
|
38
|
+
# @return [Array<String>]
|
|
39
|
+
def palette(*colors)
|
|
40
|
+
@params[:palette] = normalize_palette(colors)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Stores one-argument style setters into `params`.
|
|
44
|
+
# @api private
|
|
45
|
+
def method_missing(method_name, *args, &block)
|
|
46
|
+
if block.nil? && args.length == 1
|
|
47
|
+
@params[method_name.to_sym] = args.first
|
|
48
|
+
return args.first
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def normalize_palette(colors)
|
|
61
|
+
values = colors.flatten.map { |color| color.to_s.strip }.reject(&:empty?)
|
|
62
|
+
raise ArgumentError, "#{@kind} #{@name} palette requires at least one color" if values.empty?
|
|
63
|
+
|
|
64
|
+
values
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|