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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +70 -117
  3. data/docs/.nojekyll +0 -0
  4. data/docs/assets/playground-worker.js +373 -0
  5. data/docs/assets/playground.css +440 -0
  6. data/docs/assets/playground.js +652 -0
  7. data/docs/assets/site.css +744 -0
  8. data/docs/assets/vizcore-demo.gif +0 -0
  9. data/docs/assets/vizcore-poster.png +0 -0
  10. data/docs/assets/vj-tunnel.js +159 -0
  11. data/docs/index.html +225 -0
  12. data/docs/playground.html +81 -0
  13. data/docs/shape_dsl.md +269 -0
  14. data/examples/README.md +59 -0
  15. data/examples/assets/README.md +19 -0
  16. data/examples/audio_inspector.rb +34 -0
  17. data/examples/club_intro_drop.rb +78 -0
  18. data/examples/kansai_rubykaigi_visual.rb +70 -0
  19. data/examples/live_coding_minimal.rb +22 -0
  20. data/examples/midi_controller_show.rb +78 -0
  21. data/examples/midi_scene_switch.rb +3 -1
  22. data/examples/parser_visualizer.rb +48 -0
  23. data/examples/readme_demo.rb +17 -0
  24. data/examples/rhythm_geometry.rb +34 -0
  25. data/examples/ruby_crystal_show.rb +35 -0
  26. data/examples/shader_playground.rb +18 -0
  27. data/examples/unyo_liquid.rb +59 -0
  28. data/examples/vj_ambient_chill_room.rb +124 -0
  29. data/examples/vj_dnb_jungle.rb +170 -0
  30. data/examples/vj_festival_mainstage.rb +245 -0
  31. data/examples/vj_festival_mainstage.yml +17 -0
  32. data/examples/vj_glitch_industrial.rb +164 -0
  33. data/examples/vj_hiphop_cipher.rb +167 -0
  34. data/examples/vj_jpop_idol_live.rb +210 -0
  35. data/examples/vj_synthwave_retro.rb +173 -0
  36. data/examples/vj_techno_warehouse.rb +195 -0
  37. data/frontend/index.html +494 -2
  38. data/frontend/src/audio-inspector.js +40 -0
  39. data/frontend/src/custom-shape-param-controls.js +106 -0
  40. data/frontend/src/live-controls.js +131 -0
  41. data/frontend/src/main.js +1060 -16
  42. data/frontend/src/mapping-target-selector.js +109 -0
  43. data/frontend/src/midi-learn.js +194 -0
  44. data/frontend/src/performance-monitor.js +183 -0
  45. data/frontend/src/plugin-runtime.js +130 -0
  46. data/frontend/src/projector-mode.js +56 -0
  47. data/frontend/src/renderer/engine.js +157 -3
  48. data/frontend/src/renderer/layer-manager.js +442 -30
  49. data/frontend/src/renderer/shader-manager.js +26 -0
  50. data/frontend/src/runtime-control-preset.js +11 -0
  51. data/frontend/src/shader-error-overlay.js +29 -0
  52. data/frontend/src/shader-param-controls.js +93 -0
  53. data/frontend/src/shaders/builtins.js +380 -2
  54. data/frontend/src/shaders/post-effects.js +52 -0
  55. data/frontend/src/shape-editor-controls.js +157 -0
  56. data/frontend/src/visual-regression.js +67 -0
  57. data/frontend/src/visual-settings-preset.js +103 -0
  58. data/frontend/src/visuals/geometry.js +666 -0
  59. data/frontend/src/visuals/image-renderer.js +291 -0
  60. data/frontend/src/visuals/particle-system.js +56 -10
  61. data/frontend/src/visuals/shape-renderer.js +475 -0
  62. data/frontend/src/visuals/spectrogram-renderer.js +226 -0
  63. data/frontend/src/visuals/svg-arc.js +104 -0
  64. data/frontend/src/visuals/text-renderer.js +112 -11
  65. data/frontend/src/websocket-client.js +12 -1
  66. data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
  67. data/lib/vizcore/analysis/beat_detector.rb +4 -2
  68. data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
  69. data/lib/vizcore/analysis/feature_recorder.rb +159 -0
  70. data/lib/vizcore/analysis/feature_replay.rb +84 -0
  71. data/lib/vizcore/analysis/pipeline.rb +235 -11
  72. data/lib/vizcore/analysis/tap_tempo.rb +74 -0
  73. data/lib/vizcore/analysis.rb +4 -0
  74. data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
  75. data/lib/vizcore/audio/fixture_input.rb +65 -0
  76. data/lib/vizcore/audio/input_manager.rb +4 -2
  77. data/lib/vizcore/audio/mic_input.rb +24 -8
  78. data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
  79. data/lib/vizcore/audio.rb +1 -0
  80. data/lib/vizcore/cli/doctor.rb +159 -0
  81. data/lib/vizcore/cli/dsl_reference.rb +99 -0
  82. data/lib/vizcore/cli/layer_docs.rb +46 -0
  83. data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
  84. data/lib/vizcore/cli/scene_inspector.rb +136 -0
  85. data/lib/vizcore/cli/scene_validator.rb +337 -0
  86. data/lib/vizcore/cli/shader_template.rb +68 -0
  87. data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
  88. data/lib/vizcore/cli.rb +689 -18
  89. data/lib/vizcore/config.rb +103 -2
  90. data/lib/vizcore/control_preset.rb +68 -0
  91. data/lib/vizcore/dsl/engine.rb +277 -5
  92. data/lib/vizcore/dsl/layer_builder.rb +1280 -23
  93. data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
  94. data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
  95. data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
  96. data/lib/vizcore/dsl/reaction_builder.rb +44 -0
  97. data/lib/vizcore/dsl/scene_builder.rb +61 -5
  98. data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
  99. data/lib/vizcore/dsl/style_builder.rb +68 -0
  100. data/lib/vizcore/dsl/timeline_builder.rb +138 -0
  101. data/lib/vizcore/dsl/transition_controller.rb +77 -0
  102. data/lib/vizcore/dsl.rb +5 -1
  103. data/lib/vizcore/layer_catalog.rb +275 -0
  104. data/lib/vizcore/project_manifest.rb +152 -0
  105. data/lib/vizcore/renderer/png_writer.rb +57 -0
  106. data/lib/vizcore/renderer/render_sequence.rb +153 -0
  107. data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
  108. data/lib/vizcore/renderer/scene_serializer.rb +36 -3
  109. data/lib/vizcore/renderer/snapshot.rb +38 -0
  110. data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
  111. data/lib/vizcore/renderer.rb +5 -0
  112. data/lib/vizcore/server/frame_broadcaster.rb +143 -8
  113. data/lib/vizcore/server/gallery_app.rb +155 -0
  114. data/lib/vizcore/server/gallery_page.rb +100 -0
  115. data/lib/vizcore/server/gallery_runner.rb +48 -0
  116. data/lib/vizcore/server/rack_app.rb +203 -4
  117. data/lib/vizcore/server/runner.rb +391 -22
  118. data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
  119. data/lib/vizcore/server/websocket_handler.rb +60 -10
  120. data/lib/vizcore/server.rb +4 -0
  121. data/lib/vizcore/shape.rb +719 -0
  122. data/lib/vizcore/sync/osc_message.rb +103 -0
  123. data/lib/vizcore/sync/osc_receiver.rb +68 -0
  124. data/lib/vizcore/sync.rb +4 -0
  125. data/lib/vizcore/templates/midi_control_scene.rb +3 -1
  126. data/lib/vizcore/templates/plugin_layer.rb +20 -0
  127. data/lib/vizcore/templates/plugin_readme.md +23 -0
  128. data/lib/vizcore/templates/plugin_renderer.js +43 -0
  129. data/lib/vizcore/templates/plugin_scene.rb +14 -0
  130. data/lib/vizcore/templates/project_readme.md +7 -23
  131. data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
  132. data/lib/vizcore/version.rb +1 -1
  133. data/lib/vizcore.rb +28 -0
  134. data/scripts/browser_capture.mjs +75 -0
  135. data/sig/vizcore.rbs +461 -0
  136. metadata +94 -3
  137. 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