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,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require_relative "../../vizcore"
|
|
5
|
+
require_relative "../dsl"
|
|
6
|
+
require_relative "../layer_catalog"
|
|
7
|
+
|
|
8
|
+
module Vizcore
|
|
9
|
+
module CLISupport
|
|
10
|
+
# Validates scene DSL files without starting the realtime server.
|
|
11
|
+
class SceneValidator
|
|
12
|
+
BUILTIN_SHADERS = Vizcore::LayerCatalog::BUILTIN_SHADERS
|
|
13
|
+
|
|
14
|
+
MAPPING_SOURCE_KINDS = %i[
|
|
15
|
+
amplitude frequency_band fft_spectrum onset kick snare hihat beat beat_confidence beat_pulse beat_count bpm
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
FREQUENCY_BANDS = %i[sub low mid high].freeze
|
|
19
|
+
SUPPORTED_BLEND_MODES = Vizcore::LayerCatalog::BLEND_MODES
|
|
20
|
+
SUPPORTED_POST_EFFECTS = Vizcore::LayerCatalog::POST_EFFECTS
|
|
21
|
+
SUPPORTED_VJ_EFFECTS = Vizcore::LayerCatalog::VJ_EFFECTS
|
|
22
|
+
SUPPORTED_SHAPE_KINDS = %i[circle line rect polygon polyline path star].freeze
|
|
23
|
+
|
|
24
|
+
Issue = Struct.new(:severity, :message, keyword_init: true) do
|
|
25
|
+
def error?
|
|
26
|
+
severity == :error
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Result = Struct.new(:definition, :issues, keyword_init: true) do
|
|
31
|
+
def valid?
|
|
32
|
+
issues.none?(&:error?)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def errors
|
|
36
|
+
issues.select(&:error?)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def warnings
|
|
40
|
+
issues.reject(&:error?)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(scene_file:, loader: Vizcore::DSL::Engine.method(:load_file), shader_resolver: Vizcore::DSL::ShaderSourceResolver.new)
|
|
45
|
+
@scene_file = scene_file
|
|
46
|
+
@loader = loader
|
|
47
|
+
@shader_resolver = shader_resolver
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def call
|
|
51
|
+
definition = load_definition
|
|
52
|
+
Result.new(definition: definition, issues: validate_definition(definition))
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
Result.new(
|
|
55
|
+
definition: nil,
|
|
56
|
+
issues: [Issue.new(severity: :error, message: "failed to load scene: #{e.message}")]
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def load_definition
|
|
63
|
+
definition = @loader.call(@scene_file)
|
|
64
|
+
@shader_resolver.resolve(definition: definition, scene_file: @scene_file)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate_definition(definition)
|
|
68
|
+
issues = []
|
|
69
|
+
scenes = Array(definition[:scenes])
|
|
70
|
+
validate_scenes(scenes, issues)
|
|
71
|
+
names = scene_names(scenes)
|
|
72
|
+
validate_transitions(Array(definition[:transitions]), names, issues)
|
|
73
|
+
validate_key_mappings(Array(definition[:key_mappings]), names, issues)
|
|
74
|
+
issues
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_scenes(scenes, issues)
|
|
78
|
+
issues << error("no scenes defined") if scenes.empty?
|
|
79
|
+
duplicate_values(scenes.filter_map { |scene| scene[:name]&.to_sym }).each do |name|
|
|
80
|
+
issues << error("duplicate scene name: #{name}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
scenes.each do |scene|
|
|
84
|
+
scene_name = scene[:name] || "(unnamed)"
|
|
85
|
+
layers = Array(scene[:layers])
|
|
86
|
+
issues << warn("scene #{scene_name} has no layers; frontend will render the default geometry") if layers.empty?
|
|
87
|
+
validate_layers(layers, scene_name, issues)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_layers(layers, scene_name, issues)
|
|
92
|
+
duplicate_values(layers.filter_map { |layer| layer[:name]&.to_sym }).each do |name|
|
|
93
|
+
issues << warn("scene #{scene_name} has duplicate layer name: #{name}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
layers.each do |layer|
|
|
97
|
+
validate_layer(layer, scene_name, issues)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def validate_layer(layer, scene_name, issues)
|
|
102
|
+
layer_name = layer[:name] || "(unnamed)"
|
|
103
|
+
type = layer[:type]&.to_sym || :geometry
|
|
104
|
+
unless supported_layer_types.include?(type)
|
|
105
|
+
issues << error("scene #{scene_name} layer #{layer_name} has unsupported type: #{type}")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
shader = layer[:shader]&.to_sym
|
|
109
|
+
if shader && !BUILTIN_SHADERS.include?(shader)
|
|
110
|
+
issues << error("scene #{scene_name} layer #{layer_name} uses unknown shader: #{shader}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
glsl_source = layer[:glsl_source]
|
|
114
|
+
issues << warn("scene #{scene_name} layer #{layer_name} has an empty GLSL file") if layer[:glsl] && glsl_source.to_s.empty?
|
|
115
|
+
validate_blend_mode(layer, scene_name, layer_name, issues)
|
|
116
|
+
validate_layer_effects(layer, scene_name, layer_name, issues)
|
|
117
|
+
validate_shape_layer(layer, scene_name, layer_name, issues)
|
|
118
|
+
validate_mappings(Array(layer[:mappings]), scene_name, layer_name, issues)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def validate_blend_mode(layer, scene_name, layer_name, issues)
|
|
122
|
+
blend = layer.dig(:params, :blend)
|
|
123
|
+
return unless blend
|
|
124
|
+
return if SUPPORTED_BLEND_MODES.include?(blend.to_sym)
|
|
125
|
+
|
|
126
|
+
issues << error("scene #{scene_name} layer #{layer_name} uses unsupported blend mode: #{blend}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_layer_effects(layer, scene_name, layer_name, issues)
|
|
130
|
+
params = layer[:params] || {}
|
|
131
|
+
validate_effect_name(params[:effect], SUPPORTED_POST_EFFECTS, "effect", scene_name, layer_name, issues)
|
|
132
|
+
validate_effect_name(params[:vj_effect], SUPPORTED_VJ_EFFECTS, "vj_effect", scene_name, layer_name, issues)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validate_shape_layer(layer, scene_name, layer_name, issues)
|
|
136
|
+
params = layer[:params] || {}
|
|
137
|
+
return unless shape_layer?(layer) || shape_value(params, :shapes)
|
|
138
|
+
|
|
139
|
+
Array(shape_value(params, :shapes)).each_with_index do |shape, index|
|
|
140
|
+
values = shape_hash(shape)
|
|
141
|
+
label = shape_label(values, index)
|
|
142
|
+
validate_shape_kind(values, label, scene_name, layer_name, issues)
|
|
143
|
+
validate_shape_fallback_fill(values, label, scene_name, layer_name, issues)
|
|
144
|
+
validate_shape_opacity(values, label, scene_name, layer_name, issues)
|
|
145
|
+
validate_shape_scale(values, label, scene_name, layer_name, issues)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def validate_shape_kind(shape, label, scene_name, layer_name, issues)
|
|
150
|
+
kind = shape_value(shape, :kind)&.to_sym
|
|
151
|
+
return if SUPPORTED_SHAPE_KINDS.include?(kind)
|
|
152
|
+
|
|
153
|
+
issues << warn("scene #{scene_name} layer #{layer_name} shape #{label} uses unsupported kind: #{kind || "missing"}")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def validate_shape_fallback_fill(shape, label, scene_name, layer_name, issues)
|
|
157
|
+
fill = shape_value(shape, :fill)
|
|
158
|
+
return if fill.nil? || fill.to_s.empty?
|
|
159
|
+
|
|
160
|
+
issues << warn("scene #{scene_name} layer #{layer_name} shape #{label} fill may be ignored by line fallback")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validate_shape_opacity(shape, label, scene_name, layer_name, issues)
|
|
164
|
+
opacity = numeric_shape_value(shape_value(shape, :opacity))
|
|
165
|
+
return unless opacity && (opacity.negative? || opacity > 1)
|
|
166
|
+
|
|
167
|
+
issues << warn("scene #{scene_name} layer #{layer_name} shape #{label} opacity #{opacity} is outside 0..1; renderer will clamp")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def validate_shape_scale(shape, label, scene_name, layer_name, issues)
|
|
171
|
+
scale_values(shape).each do |scale|
|
|
172
|
+
next unless scale
|
|
173
|
+
|
|
174
|
+
if scale.zero?
|
|
175
|
+
issues << warn("scene #{scene_name} layer #{layer_name} shape #{label} scale includes 0; shape may collapse")
|
|
176
|
+
elsif scale.abs > 8
|
|
177
|
+
issues << warn("scene #{scene_name} layer #{layer_name} shape #{label} scale #{scale} is extreme; renderer will clamp")
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def supported_layer_types
|
|
183
|
+
Vizcore::LayerCatalog.supported_types
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def shape_layer?(layer)
|
|
187
|
+
%w[shape shapes shape_layer].include?((layer[:type] || layer["type"]).to_s)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def shape_label(shape, index)
|
|
191
|
+
id = shape_value(shape, :id)
|
|
192
|
+
id ? "`#{id}`" : "##{index + 1}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def scale_values(shape)
|
|
196
|
+
transform = Hash(shape_value(shape, :transform) || {})
|
|
197
|
+
scale = shape_value(transform, :scale) || shape_value(shape, :scale)
|
|
198
|
+
case scale
|
|
199
|
+
when Hash
|
|
200
|
+
[numeric_shape_value(shape_value(scale, :x)), numeric_shape_value(shape_value(scale, :y))]
|
|
201
|
+
when Array
|
|
202
|
+
[numeric_shape_value(scale[0]), numeric_shape_value(scale[1])]
|
|
203
|
+
else
|
|
204
|
+
[numeric_shape_value(scale)]
|
|
205
|
+
end
|
|
206
|
+
rescue TypeError
|
|
207
|
+
[]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def numeric_shape_value(value)
|
|
211
|
+
return nil if value.nil?
|
|
212
|
+
|
|
213
|
+
numeric = Float(value)
|
|
214
|
+
numeric if numeric.finite?
|
|
215
|
+
rescue ArgumentError, TypeError
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def shape_hash(value)
|
|
220
|
+
Hash(value)
|
|
221
|
+
rescue TypeError
|
|
222
|
+
{}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def shape_value(hash, key)
|
|
226
|
+
hash[key] || hash[key.to_s]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def validate_effect_name(value, supported, field, scene_name, layer_name, issues)
|
|
230
|
+
return unless value
|
|
231
|
+
return if supported.include?(value.to_sym)
|
|
232
|
+
|
|
233
|
+
issues << error("scene #{scene_name} layer #{layer_name} uses unsupported #{field}: #{value}")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def validate_mappings(mappings, scene_name, layer_name, issues)
|
|
237
|
+
mappings.each do |mapping|
|
|
238
|
+
source = Hash(mapping[:source] || {})
|
|
239
|
+
kind = source[:kind]&.to_sym
|
|
240
|
+
issues << error("scene #{scene_name} layer #{layer_name} has mapping without source kind") unless kind
|
|
241
|
+
next unless kind
|
|
242
|
+
|
|
243
|
+
validate_mapping_source(kind, source, scene_name, layer_name, issues)
|
|
244
|
+
issues << error("scene #{scene_name} layer #{layer_name} has mapping without target") unless mapping[:target]
|
|
245
|
+
validate_transform(Hash(mapping[:transform] || {}), scene_name, layer_name, mapping[:target], issues)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def validate_mapping_source(kind, source, scene_name, layer_name, issues)
|
|
250
|
+
unless MAPPING_SOURCE_KINDS.include?(kind)
|
|
251
|
+
issues << error("scene #{scene_name} layer #{layer_name} uses unsupported mapping source: #{kind}")
|
|
252
|
+
end
|
|
253
|
+
validate_frequency_band(source, scene_name, layer_name, issues) if kind == :frequency_band
|
|
254
|
+
validate_onset_band(source, scene_name, layer_name, issues) if kind == :onset
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def validate_frequency_band(source, scene_name, layer_name, issues)
|
|
258
|
+
band = source[:band]&.to_sym
|
|
259
|
+
return if FREQUENCY_BANDS.include?(band)
|
|
260
|
+
|
|
261
|
+
issues << error("scene #{scene_name} layer #{layer_name} uses unsupported frequency band: #{band.inspect}")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def validate_onset_band(source, scene_name, layer_name, issues)
|
|
265
|
+
return unless source.key?(:band)
|
|
266
|
+
|
|
267
|
+
band = source[:band]&.to_sym
|
|
268
|
+
return if FREQUENCY_BANDS.include?(band)
|
|
269
|
+
|
|
270
|
+
issues << error("scene #{scene_name} layer #{layer_name} uses unsupported onset band: #{band.inspect}")
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def validate_transform(transform, scene_name, layer_name, target, issues)
|
|
274
|
+
return unless transform.key?(:min) && transform.key?(:max)
|
|
275
|
+
return unless Float(transform[:min]) > Float(transform[:max])
|
|
276
|
+
|
|
277
|
+
issues << error("scene #{scene_name} layer #{layer_name} mapping #{target} has min greater than max")
|
|
278
|
+
rescue ArgumentError, TypeError
|
|
279
|
+
issues << error("scene #{scene_name} layer #{layer_name} mapping #{target} has non-numeric min/max")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def validate_transitions(transitions, names, issues)
|
|
283
|
+
transitions.each do |transition|
|
|
284
|
+
from = transition[:from]&.to_sym
|
|
285
|
+
to = transition[:to]&.to_sym
|
|
286
|
+
issues << error("transition has unknown source scene: #{from}") if from && !names.include?(from)
|
|
287
|
+
issues << error("transition has unknown target scene: #{to}") if to && !names.include?(to)
|
|
288
|
+
unless transition[:trigger].respond_to?(:call)
|
|
289
|
+
issues << warn("transition #{from || '?'} -> #{to || '?'} has no trigger block")
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def validate_key_mappings(mappings, names, issues)
|
|
295
|
+
mappings.each do |mapping|
|
|
296
|
+
key = mapping[:key] || mapping["key"]
|
|
297
|
+
action = mapping[:action] || mapping["action"]
|
|
298
|
+
issues << error("key mapping has empty key") if key.to_s.strip.empty?
|
|
299
|
+
validate_key_action(action.is_a?(Hash) ? action : {}, names, key, issues)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def validate_key_action(action, names, key, issues)
|
|
304
|
+
type = (action[:type] || action["type"]).to_s.to_sym
|
|
305
|
+
case type
|
|
306
|
+
when :switch_scene
|
|
307
|
+
scene = action[:scene] || action["scene"]
|
|
308
|
+
scene_name = scene&.to_sym
|
|
309
|
+
issues << error("key #{key} switches to unknown scene: #{scene}") unless scene_name && names.include?(scene_name)
|
|
310
|
+
when :live_control
|
|
311
|
+
control = (action[:control] || action["control"]).to_s.to_sym
|
|
312
|
+
issues << error("key #{key} uses unsupported live control: #{control}") unless %i[blackout freeze].include?(control)
|
|
313
|
+
else
|
|
314
|
+
issues << error("key #{key} has unsupported action: #{type}")
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def scene_names(scenes)
|
|
319
|
+
scenes.filter_map { |scene| scene[:name]&.to_sym }.to_set
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def duplicate_values(values)
|
|
323
|
+
counts = Hash.new(0)
|
|
324
|
+
values.each { |value| counts[value] += 1 }
|
|
325
|
+
counts.select { |_value, count| count > 1 }.keys
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def error(message)
|
|
329
|
+
Issue.new(severity: :error, message: message)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def warn(message)
|
|
333
|
+
Issue.new(severity: :warn, message: message)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Vizcore
|
|
7
|
+
module CLISupport
|
|
8
|
+
# Writes starter GLSL ES fragment shaders for custom shader layers.
|
|
9
|
+
class ShaderTemplate
|
|
10
|
+
TEMPLATE = <<~GLSL
|
|
11
|
+
#version 300 es
|
|
12
|
+
precision highp float;
|
|
13
|
+
|
|
14
|
+
uniform vec2 u_resolution;
|
|
15
|
+
uniform float u_time;
|
|
16
|
+
uniform float u_amplitude;
|
|
17
|
+
uniform float u_bass;
|
|
18
|
+
uniform float u_mid;
|
|
19
|
+
uniform float u_high;
|
|
20
|
+
uniform float u_beat;
|
|
21
|
+
uniform float u_beat_pulse;
|
|
22
|
+
uniform float u_onset;
|
|
23
|
+
uniform float u_kick;
|
|
24
|
+
uniform float u_bpm;
|
|
25
|
+
uniform float u_fft[32];
|
|
26
|
+
uniform float u_fft_size;
|
|
27
|
+
|
|
28
|
+
out vec4 outColor;
|
|
29
|
+
|
|
30
|
+
void main() {
|
|
31
|
+
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
|
32
|
+
float wave = 0.5 + 0.5 * sin((uv.x + u_time * 0.12) * 12.0 + u_bass * 4.0);
|
|
33
|
+
vec3 color = mix(vec3(0.02, 0.06, 0.12), vec3(0.1, 0.75, 0.95), wave);
|
|
34
|
+
color += vec3(0.95, 0.16, 0.32) * (u_beat_pulse * 0.35 + u_onset * 0.2 + u_kick * 0.25 + u_high * 0.2);
|
|
35
|
+
color *= 0.35 + u_amplitude * 1.8;
|
|
36
|
+
outColor = vec4(color, 1.0);
|
|
37
|
+
}
|
|
38
|
+
GLSL
|
|
39
|
+
|
|
40
|
+
# @param name [String]
|
|
41
|
+
# @return [String]
|
|
42
|
+
def self.default_path(name)
|
|
43
|
+
"shaders/#{safe_name(name)}.frag"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param name [String]
|
|
47
|
+
# @return [String]
|
|
48
|
+
def self.safe_name(name)
|
|
49
|
+
raw_name = name.to_s.strip.sub(/\.frag\z/i, "")
|
|
50
|
+
safe = raw_name.gsub(/[^a-zA-Z0-9_-]+/, "_").gsub(/\A_+|_+\z/, "")
|
|
51
|
+
raise ArgumentError, "shader name is required" if safe.empty?
|
|
52
|
+
|
|
53
|
+
safe
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param path [String, Pathname]
|
|
57
|
+
# @return [Pathname]
|
|
58
|
+
def write(path)
|
|
59
|
+
destination = Pathname(path)
|
|
60
|
+
raise ArgumentError, "Shader file already exists: #{destination}" if destination.exist?
|
|
61
|
+
|
|
62
|
+
FileUtils.mkdir_p(destination.dirname)
|
|
63
|
+
destination.write(TEMPLATE)
|
|
64
|
+
destination
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vizcore
|
|
4
|
+
module CLISupport
|
|
5
|
+
# Produces the custom GLSL uniform reference used by CLI and docs.
|
|
6
|
+
class ShaderUniformDocs
|
|
7
|
+
Uniform = Struct.new(:name, :type, :description, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
UNIFORMS = [
|
|
10
|
+
Uniform.new(name: "u_resolution", type: "vec2", description: "Canvas size in pixels as width and height."),
|
|
11
|
+
Uniform.new(name: "u_time", type: "float", description: "Renderer time in seconds."),
|
|
12
|
+
Uniform.new(name: "u_amplitude", type: "float", description: "Normalized RMS amplitude, usually 0.0..1.0."),
|
|
13
|
+
Uniform.new(name: "u_bass", type: "float", description: "Low-frequency band level."),
|
|
14
|
+
Uniform.new(name: "u_mid", type: "float", description: "Mid-frequency band level."),
|
|
15
|
+
Uniform.new(name: "u_high", type: "float", description: "High-frequency band level."),
|
|
16
|
+
Uniform.new(name: "u_beat", type: "float", description: "1.0 on detected beat frames, otherwise 0.0."),
|
|
17
|
+
Uniform.new(name: "u_beat_pulse", type: "float", description: "Decaying beat pulse after detection."),
|
|
18
|
+
Uniform.new(name: "u_onset", type: "float", description: "Positive amplitude rise since the previous active frame."),
|
|
19
|
+
Uniform.new(name: "u_sub_onset", type: "float", description: "Positive sub-band rise since the previous active frame."),
|
|
20
|
+
Uniform.new(name: "u_low_onset", type: "float", description: "Positive low-band rise since the previous active frame."),
|
|
21
|
+
Uniform.new(name: "u_mid_onset", type: "float", description: "Positive mid-band rise since the previous active frame."),
|
|
22
|
+
Uniform.new(name: "u_high_onset", type: "float", description: "Positive high-band rise since the previous active frame."),
|
|
23
|
+
Uniform.new(name: "u_kick", type: "float", description: "Low-band percussive confidence derived from band level and onset."),
|
|
24
|
+
Uniform.new(name: "u_snare", type: "float", description: "Mid-band percussive confidence derived from band level and onset."),
|
|
25
|
+
Uniform.new(name: "u_hihat", type: "float", description: "High-band percussive confidence derived from band level and onset."),
|
|
26
|
+
Uniform.new(name: "u_bpm", type: "float", description: "Current BPM estimate, or 0.0 when unavailable."),
|
|
27
|
+
Uniform.new(name: "u_fft[32]", type: "float[]", description: "Normalized FFT preview bins."),
|
|
28
|
+
Uniform.new(name: "u_fft_size", type: "float", description: "Number of populated FFT preview bins."),
|
|
29
|
+
Uniform.new(name: "u_visual_gain", type: "float", description: "Browser visual gain control value."),
|
|
30
|
+
Uniform.new(name: "u_bass_boost", type: "float", description: "Browser bass boost control value."),
|
|
31
|
+
Uniform.new(name: "u_wobble_amount", type: "float", description: "Browser wobble amount control value."),
|
|
32
|
+
Uniform.new(name: "u_param_<name>", type: "float", description: "Numeric layer param or mapped DSL target."),
|
|
33
|
+
Uniform.new(name: "u_global_<name>", type: "float", description: "Numeric runtime global from DSL or MIDI set.")
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
# @return [Array<String>]
|
|
37
|
+
def lines
|
|
38
|
+
[
|
|
39
|
+
"# Vizcore Shader Uniforms",
|
|
40
|
+
"",
|
|
41
|
+
"Custom fragment shaders use GLSL ES 3.00 and receive these uniforms:",
|
|
42
|
+
"",
|
|
43
|
+
"| Uniform | Type | Description |",
|
|
44
|
+
"| --- | --- | --- |",
|
|
45
|
+
*UNIFORMS.map { |uniform| "| `#{uniform.name}` | `#{uniform.type}` | #{uniform.description} |" },
|
|
46
|
+
"",
|
|
47
|
+
"Layer params are exposed as `u_param_<name>` after non-word characters are converted to underscores.",
|
|
48
|
+
"For compatibility, a mapped target like `:param_intensity` is also exposed as `u_param_intensity`.",
|
|
49
|
+
"Runtime globals are exposed as `u_global_<name>`; `:global_intensity` becomes `u_global_intensity`."
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|