vizcore 0.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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +170 -0
  4. data/docs/GETTING_STARTED.md +105 -0
  5. data/examples/assets/complex_demo_loop.wav +0 -0
  6. data/examples/basic.rb +9 -0
  7. data/examples/complex_audio_showcase.rb +261 -0
  8. data/examples/custom_shader.rb +21 -0
  9. data/examples/file_audio_demo.rb +74 -0
  10. data/examples/intro_drop.rb +38 -0
  11. data/examples/midi_scene_switch.rb +32 -0
  12. data/examples/shaders/custom_wave.frag +30 -0
  13. data/exe/vizcore +6 -0
  14. data/frontend/index.html +148 -0
  15. data/frontend/src/main.js +304 -0
  16. data/frontend/src/renderer/engine.js +135 -0
  17. data/frontend/src/renderer/layer-manager.js +456 -0
  18. data/frontend/src/renderer/shader-manager.js +69 -0
  19. data/frontend/src/shaders/builtins.js +244 -0
  20. data/frontend/src/shaders/post-effects.js +85 -0
  21. data/frontend/src/visuals/geometry.js +66 -0
  22. data/frontend/src/visuals/particle-system.js +148 -0
  23. data/frontend/src/visuals/text-renderer.js +143 -0
  24. data/frontend/src/visuals/vj-effects.js +56 -0
  25. data/frontend/src/websocket-client.js +131 -0
  26. data/lib/vizcore/analysis/band_splitter.rb +63 -0
  27. data/lib/vizcore/analysis/beat_detector.rb +70 -0
  28. data/lib/vizcore/analysis/bpm_estimator.rb +86 -0
  29. data/lib/vizcore/analysis/fft_processor.rb +224 -0
  30. data/lib/vizcore/analysis/fftw_ffi.rb +50 -0
  31. data/lib/vizcore/analysis/pipeline.rb +72 -0
  32. data/lib/vizcore/analysis/smoother.rb +74 -0
  33. data/lib/vizcore/analysis.rb +14 -0
  34. data/lib/vizcore/audio/base_input.rb +39 -0
  35. data/lib/vizcore/audio/dummy_sine_input.rb +40 -0
  36. data/lib/vizcore/audio/file_input.rb +163 -0
  37. data/lib/vizcore/audio/input_manager.rb +133 -0
  38. data/lib/vizcore/audio/mic_input.rb +121 -0
  39. data/lib/vizcore/audio/midi_input.rb +246 -0
  40. data/lib/vizcore/audio/portaudio_ffi.rb +243 -0
  41. data/lib/vizcore/audio/ring_buffer.rb +92 -0
  42. data/lib/vizcore/audio.rb +16 -0
  43. data/lib/vizcore/cli.rb +115 -0
  44. data/lib/vizcore/config.rb +46 -0
  45. data/lib/vizcore/dsl/engine.rb +229 -0
  46. data/lib/vizcore/dsl/file_watcher.rb +108 -0
  47. data/lib/vizcore/dsl/layer_builder.rb +182 -0
  48. data/lib/vizcore/dsl/mapping_resolver.rb +81 -0
  49. data/lib/vizcore/dsl/midi_map_executor.rb +188 -0
  50. data/lib/vizcore/dsl/scene_builder.rb +44 -0
  51. data/lib/vizcore/dsl/shader_source_resolver.rb +71 -0
  52. data/lib/vizcore/dsl/transition_controller.rb +166 -0
  53. data/lib/vizcore/dsl.rb +16 -0
  54. data/lib/vizcore/errors.rb +27 -0
  55. data/lib/vizcore/renderer/frame_scheduler.rb +75 -0
  56. data/lib/vizcore/renderer/scene_serializer.rb +73 -0
  57. data/lib/vizcore/renderer.rb +10 -0
  58. data/lib/vizcore/server/frame_broadcaster.rb +351 -0
  59. data/lib/vizcore/server/rack_app.rb +183 -0
  60. data/lib/vizcore/server/runner.rb +357 -0
  61. data/lib/vizcore/server/websocket_handler.rb +163 -0
  62. data/lib/vizcore/server.rb +12 -0
  63. data/lib/vizcore/templates/basic_scene.rb +10 -0
  64. data/lib/vizcore/templates/custom_shader_scene.rb +22 -0
  65. data/lib/vizcore/templates/custom_wave.frag +31 -0
  66. data/lib/vizcore/templates/intro_drop_scene.rb +40 -0
  67. data/lib/vizcore/templates/midi_control_scene.rb +33 -0
  68. data/lib/vizcore/templates/project_readme.md +35 -0
  69. data/lib/vizcore/version.rb +6 -0
  70. data/lib/vizcore.rb +37 -0
  71. metadata +186 -0
@@ -0,0 +1,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "puma"
4
+ require_relative "../config"
5
+ require_relative "../dsl"
6
+ require_relative "../errors"
7
+ require_relative "frame_broadcaster"
8
+ require_relative "rack_app"
9
+ require_relative "websocket_handler"
10
+
11
+ module Vizcore
12
+ module Server
13
+ # Bootstraps Rack/Puma, audio pipeline, scene reload, and MIDI runtime.
14
+ class Runner
15
+ # @param config [Vizcore::Config]
16
+ # @param output [#puts]
17
+ def initialize(config, output: $stdout)
18
+ @config = config
19
+ @output = output
20
+ @shader_source_resolver = Vizcore::DSL::ShaderSourceResolver.new
21
+ @scene_catalog_mutex = Mutex.new
22
+ @scene_catalog = []
23
+ end
24
+
25
+ # Run server lifecycle until interrupted.
26
+ #
27
+ # @raise [Vizcore::ConfigurationError]
28
+ # @raise [Vizcore::SceneLoadError]
29
+ # @return [void]
30
+ def run
31
+ validate_scene_file!
32
+ validate_audio_settings!
33
+ definition = load_definition!
34
+ scene = first_scene(definition) || fallback_scene
35
+
36
+ app = RackApp.new(
37
+ frontend_root: Vizcore.frontend_root,
38
+ audio_source: @config.audio_source,
39
+ audio_file: @config.audio_file,
40
+ scene_names: scene_names_for(definition)
41
+ )
42
+ server = Puma::Server.new(app, nil, min_threads: 0, max_threads: 4)
43
+ server.add_tcp_listener(@config.host, @config.port)
44
+ server.run
45
+
46
+ input_manager = Vizcore::Audio::InputManager.new(
47
+ source: @config.audio_source,
48
+ file_path: @config.audio_file&.to_s
49
+ )
50
+ broadcaster = FrameBroadcaster.new(
51
+ scene_name: scene[:name].to_s,
52
+ scene_layers: scene[:layers],
53
+ scene_catalog: definition[:scenes],
54
+ transitions: definition[:transitions],
55
+ input_manager: input_manager,
56
+ error_reporter: ->(message) { @output.puts(message) }
57
+ )
58
+ replace_scene_catalog(definition[:scenes])
59
+ if @config.audio_source == :file
60
+ broadcaster.sync_transport(playing: false, position_seconds: 0.0)
61
+ end
62
+ broadcaster.start
63
+ register_client_message_handler(broadcaster)
64
+ midi_runtime = start_midi_runtime(definition, broadcaster)
65
+ watcher = start_scene_watcher(broadcaster) do |updated_definition|
66
+ midi_runtime = refresh_midi_runtime(midi_runtime, updated_definition, broadcaster)
67
+ end
68
+
69
+ @output.puts("Vizcore server listening at http://#{@config.host}:#{@config.port}")
70
+ @output.puts("Scene: #{scene[:name]}")
71
+ @output.puts("Audio playback: http://#{@config.host}:#{@config.port}/audio-file") if @config.audio_source == :file
72
+ @output.puts("Press Ctrl+C to stop.")
73
+
74
+ wait_for_interrupt
75
+ ensure
76
+ Vizcore::Server::WebSocketHandler.clear_message_handler
77
+ stop_midi_runtime(midi_runtime)
78
+ watcher&.stop
79
+ broadcaster&.stop
80
+ server&.stop(true)
81
+ end
82
+
83
+ private
84
+
85
+ def validate_scene_file!
86
+ return if @config.scene_exists?
87
+
88
+ message = if @config.scene_file
89
+ "Scene file not found: #{@config.scene_file}"
90
+ else
91
+ "Scene file is required"
92
+ end
93
+
94
+ raise Vizcore::ConfigurationError, message
95
+ end
96
+
97
+ def load_definition!
98
+ raw_definition = Vizcore::DSL::Engine.load_file(@config.scene_file.to_s)
99
+ resolve_shader_sources(raw_definition)
100
+ rescue StandardError => e
101
+ raise Vizcore::SceneLoadError, Vizcore::ErrorFormatting.summarize(
102
+ e,
103
+ context: "Failed to load scene file #{@config.scene_file}"
104
+ )
105
+ end
106
+
107
+ def validate_audio_settings!
108
+ return unless @config.audio_source == :file
109
+ return if @config.audio_file && @config.audio_file.file?
110
+
111
+ raise Vizcore::ConfigurationError, "Audio file not found: #{@config.audio_file || '(nil)'}"
112
+ end
113
+
114
+ def wait_for_interrupt
115
+ stop_requested = false
116
+ %w[INT TERM].each do |signal_name|
117
+ Signal.trap(signal_name) { stop_requested = true }
118
+ rescue ArgumentError
119
+ nil
120
+ end
121
+ sleep(0.1) until stop_requested
122
+ end
123
+
124
+ def start_scene_watcher(broadcaster, &on_reload)
125
+ watcher = Vizcore::DSL::Engine.watch_file(@config.scene_file.to_s) do |definition, _changed_path|
126
+ definition = resolve_shader_sources(definition)
127
+ replace_scene_catalog(definition[:scenes])
128
+ scene = first_scene(definition) || fallback_scene
129
+ broadcaster.update_transition_definition(
130
+ scenes: Array(definition[:scenes]),
131
+ transitions: Array(definition[:transitions])
132
+ )
133
+ broadcaster.update_scene(scene_name: scene[:name], scene_layers: scene[:layers])
134
+ on_reload&.call(definition)
135
+ WebSocketHandler.broadcast(
136
+ type: "config_update",
137
+ payload: {
138
+ scene: scene,
139
+ scenes: scene_names_for(definition)
140
+ }
141
+ )
142
+ @output.puts("Scene reloaded: #{scene[:name]}")
143
+ rescue StandardError => e
144
+ @output.puts(Vizcore::ErrorFormatting.summarize(e, context: "Scene reload failed"))
145
+ end
146
+ watcher.start
147
+ watcher
148
+ rescue StandardError => e
149
+ @output.puts(Vizcore::ErrorFormatting.summarize(e, context: "Scene watcher disabled"))
150
+ nil
151
+ end
152
+
153
+ def first_scene(definition)
154
+ definition.fetch(:scenes, []).first
155
+ end
156
+
157
+ def fallback_scene
158
+ {
159
+ name: @config.scene_file.basename(".rb").to_sym,
160
+ layers: []
161
+ }
162
+ end
163
+
164
+ def start_midi_runtime(definition, broadcaster)
165
+ settings = midi_runtime_settings(definition)
166
+ return nil unless settings[:enabled]
167
+
168
+ midi_input = Vizcore::Audio::MidiInput.new(device: settings[:device])
169
+ executor = Vizcore::DSL::MidiMapExecutor.new(
170
+ midi_maps: settings[:midi_maps],
171
+ scenes: settings[:scenes],
172
+ globals: settings[:globals]
173
+ )
174
+ midi_input.start { |event| handle_midi_event(executor, event, broadcaster) }
175
+ @output.puts("MIDI mapping enabled#{settings[:device] ? " (device=#{settings[:device]})" : ""}")
176
+
177
+ {
178
+ input: midi_input,
179
+ executor: executor,
180
+ device: settings[:device]
181
+ }
182
+ rescue StandardError => e
183
+ @output.puts(Vizcore::ErrorFormatting.summarize(e, context: "MIDI runtime disabled"))
184
+ midi_input&.stop
185
+ nil
186
+ end
187
+
188
+ def refresh_midi_runtime(runtime, definition, broadcaster)
189
+ settings = midi_runtime_settings(definition)
190
+ return stop_midi_runtime(runtime) unless settings[:enabled]
191
+ return start_midi_runtime(definition, broadcaster) unless runtime
192
+
193
+ if runtime[:device] != settings[:device]
194
+ stop_midi_runtime(runtime)
195
+ return start_midi_runtime(definition, broadcaster)
196
+ end
197
+
198
+ runtime[:executor].update(
199
+ midi_maps: settings[:midi_maps],
200
+ scenes: settings[:scenes],
201
+ globals: settings[:globals]
202
+ )
203
+ runtime
204
+ rescue StandardError => e
205
+ @output.puts(Vizcore::ErrorFormatting.summarize(e, context: "MIDI runtime update failed"))
206
+ runtime
207
+ end
208
+
209
+ def stop_midi_runtime(runtime)
210
+ return nil unless runtime
211
+
212
+ runtime[:input]&.stop
213
+ nil
214
+ rescue StandardError => e
215
+ @output.puts(Vizcore::ErrorFormatting.summarize(e, context: "MIDI runtime shutdown failed"))
216
+ nil
217
+ end
218
+
219
+ def handle_midi_event(executor, event, broadcaster)
220
+ actions = executor.handle_event(event)
221
+ actions.each do |action|
222
+ apply_midi_action(action, executor, broadcaster)
223
+ end
224
+ rescue StandardError => e
225
+ @output.puts(Vizcore::ErrorFormatting.summarize(e, context: "MIDI action failed"))
226
+ end
227
+
228
+ def register_client_message_handler(broadcaster)
229
+ Vizcore::Server::WebSocketHandler.on_message do |message|
230
+ handle_client_message(message, broadcaster)
231
+ end
232
+ end
233
+
234
+ def handle_client_message(message, broadcaster)
235
+ type = message["type"] || message[:type]
236
+ payload = message["payload"] || message[:payload]
237
+ case type.to_s
238
+ when "transport_sync"
239
+ return unless @config.audio_source == :file
240
+
241
+ values = Hash(payload)
242
+ broadcaster.sync_transport(
243
+ playing: values.fetch("playing", values.fetch(:playing, false)),
244
+ position_seconds: values.fetch("position_seconds", values.fetch(:position_seconds, 0.0))
245
+ )
246
+ when "switch_scene"
247
+ values = Hash(payload)
248
+ target_name = values.fetch("scene", values.fetch(:scene, values.fetch("scene_name", values.fetch(:scene_name, nil))))
249
+ switch_scene_from_client(target_name, broadcaster)
250
+ end
251
+ rescue StandardError => e
252
+ @output.puts(Vizcore::ErrorFormatting.summarize(e, context: "Client control message failed"))
253
+ end
254
+
255
+ def apply_midi_action(action, executor, broadcaster)
256
+ case action[:type]
257
+ when :switch_scene
258
+ target_scene = action[:scene]
259
+ return unless target_scene
260
+
261
+ current = broadcaster.current_scene_snapshot
262
+ from_scene = current[:name]
263
+ broadcaster.update_scene(scene_name: target_scene[:name], scene_layers: target_scene[:layers])
264
+ WebSocketHandler.broadcast(
265
+ type: "scene_change",
266
+ payload: {
267
+ from: from_scene.to_s,
268
+ to: target_scene[:name].to_s,
269
+ effect: action[:effect],
270
+ source: "midi"
271
+ }
272
+ )
273
+ when :set_global
274
+ WebSocketHandler.broadcast(
275
+ type: "config_update",
276
+ payload: {
277
+ globals: executor.globals
278
+ }
279
+ )
280
+ end
281
+ end
282
+
283
+ def midi_runtime_settings(definition)
284
+ midi_inputs = Array(definition[:midi])
285
+
286
+ {
287
+ enabled: !Array(definition[:midi_maps]).empty?,
288
+ midi_maps: Array(definition[:midi_maps]),
289
+ scenes: Array(definition[:scenes]),
290
+ globals: Hash(definition[:globals] || {}),
291
+ device: midi_inputs.first&.dig(:options, :device)
292
+ }
293
+ end
294
+
295
+ def resolve_shader_sources(definition)
296
+ @shader_source_resolver.resolve(definition: definition, scene_file: @config.scene_file.to_s)
297
+ end
298
+
299
+ def replace_scene_catalog(scenes)
300
+ @scene_catalog_mutex.synchronize do
301
+ @scene_catalog = Array(scenes)
302
+ end
303
+ end
304
+
305
+ def scene_names_for(definition)
306
+ Array(definition[:scenes]).filter_map do |scene|
307
+ name = scene.dig(:name) || scene["name"]
308
+ next if name.nil?
309
+
310
+ value = name.to_s.strip
311
+ next if value.empty?
312
+
313
+ value
314
+ end
315
+ rescue StandardError
316
+ []
317
+ end
318
+
319
+ def switch_scene_from_client(target_name, broadcaster)
320
+ requested = target_name.to_s.strip
321
+ return if requested.empty?
322
+
323
+ target_scene = find_scene_catalog_scene(requested)
324
+ return unless target_scene
325
+
326
+ current = broadcaster.current_scene_snapshot
327
+ from_scene = current[:name]
328
+ broadcaster.update_scene(scene_name: target_scene[:name], scene_layers: target_scene[:layers])
329
+ WebSocketHandler.broadcast(
330
+ type: "scene_change",
331
+ payload: {
332
+ from: from_scene.to_s,
333
+ to: target_scene[:name].to_s,
334
+ effect: nil,
335
+ source: "ui"
336
+ }
337
+ )
338
+ end
339
+
340
+ def find_scene_catalog_scene(name)
341
+ @scene_catalog_mutex.synchronize do
342
+ Array(@scene_catalog).each do |scene|
343
+ raw_name = scene.dig(:name) || scene["name"]
344
+ next unless raw_name
345
+ next unless raw_name.to_s == name
346
+
347
+ layers = scene.dig(:layers) || scene["layers"]
348
+ return { name: raw_name.to_sym, layers: Array(layers) }
349
+ end
350
+ nil
351
+ end
352
+ rescue StandardError
353
+ nil
354
+ end
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "set"
5
+ require "thread"
6
+ require_relative "../errors"
7
+
8
+ module Vizcore
9
+ module Server
10
+ # Stateless WebSocket endpoint manager for frame broadcast transport.
11
+ class WebSocketHandler
12
+ class << self
13
+ # Rack endpoint for WebSocket upgrade handling.
14
+ #
15
+ # @param env [Hash]
16
+ # @return [Array]
17
+ def call(env)
18
+ websocket_klass = faye_websocket_class
19
+ return dependency_error_response unless websocket_klass
20
+ return [426, text_headers, ["WebSocket upgrade required"]] unless websocket_klass.websocket?(env)
21
+
22
+ socket = websocket_klass.new(env, nil, ping: 15)
23
+
24
+ socket.on(:open) { register(socket) }
25
+ socket.on(:close) { unregister(socket) }
26
+ socket.on(:message) { |event| handle_message(socket, event.data) }
27
+
28
+ socket.rack_response
29
+ end
30
+
31
+ # Broadcast one typed payload to all active websocket clients.
32
+ #
33
+ # @param type [String]
34
+ # @param payload [Hash]
35
+ # @return [Boolean] false when websocket backend is unavailable
36
+ def broadcast(type:, payload:)
37
+ return false unless faye_websocket_class
38
+
39
+ message = JSON.generate(type: type, payload: payload)
40
+
41
+ each_socket do |socket|
42
+ send_message(socket, message)
43
+ end
44
+
45
+ true
46
+ end
47
+
48
+ # @return [Integer]
49
+ def connection_count
50
+ mutex.synchronize { sockets.size }
51
+ end
52
+
53
+ # @return [StandardError, nil]
54
+ def last_error
55
+ mutex.synchronize { @last_error }
56
+ end
57
+
58
+ # Register one inbound message handler for client -> server control messages.
59
+ #
60
+ # @yieldparam message [Hash]
61
+ # @return [void]
62
+ def on_message(&block)
63
+ mutex.synchronize { @message_handler = block }
64
+ end
65
+
66
+ # Clear inbound message handler.
67
+ #
68
+ # @return [void]
69
+ def clear_message_handler
70
+ mutex.synchronize { @message_handler = nil }
71
+ end
72
+
73
+ private
74
+
75
+ def faye_websocket_class
76
+ require "faye/websocket"
77
+ Faye::WebSocket
78
+ rescue LoadError
79
+ nil
80
+ end
81
+
82
+ def send_message(socket, message)
83
+ if event_machine_reactor_running?
84
+ EventMachine.schedule { safe_send(socket, message) }
85
+ else
86
+ safe_send(socket, message)
87
+ end
88
+ end
89
+
90
+ def safe_send(socket, message)
91
+ socket.send(message)
92
+ rescue StandardError => e
93
+ set_last_error(e)
94
+ unregister(socket)
95
+ end
96
+
97
+ def event_machine_reactor_running?
98
+ require "eventmachine"
99
+ EventMachine.reactor_running?
100
+ rescue LoadError
101
+ false
102
+ end
103
+
104
+ def dependency_error_response
105
+ [500, json_headers, [JSON.generate(error: "Missing dependency: faye-websocket")]]
106
+ end
107
+
108
+ def handle_message(_socket, raw_message)
109
+ message = JSON.parse(raw_message)
110
+ dispatch_message(message)
111
+ message
112
+ rescue JSON::ParserError => e
113
+ set_last_error(e)
114
+ nil
115
+ end
116
+
117
+ def register(socket)
118
+ mutex.synchronize { sockets << socket }
119
+ end
120
+
121
+ def unregister(socket)
122
+ mutex.synchronize { sockets.delete(socket) }
123
+ end
124
+
125
+ def each_socket(&block)
126
+ snapshot = mutex.synchronize { sockets.dup }
127
+ snapshot.each(&block)
128
+ end
129
+
130
+ def sockets
131
+ @sockets ||= Set.new
132
+ end
133
+
134
+ def mutex
135
+ @mutex ||= Mutex.new
136
+ end
137
+
138
+ def set_last_error(error)
139
+ mutex.synchronize { @last_error = error }
140
+ end
141
+
142
+ def dispatch_message(message)
143
+ handler = mutex.synchronize { @message_handler }
144
+ return unless handler
145
+ return unless message.is_a?(Hash)
146
+
147
+ handler.call(message)
148
+ rescue StandardError => e
149
+ set_last_error(e)
150
+ nil
151
+ end
152
+
153
+ def text_headers
154
+ { "content-type" => "text/plain; charset=utf-8" }
155
+ end
156
+
157
+ def json_headers
158
+ { "content-type" => "application/json; charset=utf-8" }
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ # Rack/WebSocket server runtime namespace.
5
+ module Server
6
+ end
7
+ end
8
+
9
+ require_relative "server/frame_broadcaster"
10
+ require_relative "server/rack_app"
11
+ require_relative "server/runner"
12
+ require_relative "server/websocket_handler"
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # {{project_name}} starter scene.
4
+ Vizcore.define do
5
+ scene :basic do
6
+ layer :wireframe_cube do
7
+ type :wireframe_cube
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # {{project_name}} custom GLSL shader example.
4
+ Vizcore.define do
5
+ scene :shader_art do
6
+ layer :kaleidoscope do
7
+ type :shader
8
+ glsl "../shaders/custom_wave.frag"
9
+ map amplitude => :param_intensity
10
+ map frequency_band(:low) => :param_bass
11
+ map beat? => :param_flash
12
+ end
13
+
14
+ layer :title do
15
+ type :text
16
+ content "{{project_name}}"
17
+ font_size 72
18
+ glow_strength 0.0
19
+ color "#f5f9ff"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ #version 300 es
2
+ precision mediump float;
3
+
4
+ uniform vec2 u_resolution;
5
+ uniform float u_time;
6
+ uniform float u_amplitude;
7
+ uniform float u_bass;
8
+ uniform float u_param_intensity;
9
+ uniform float u_param_bass;
10
+ uniform float u_param_flash;
11
+ out vec4 outColor;
12
+
13
+ void main() {
14
+ vec2 uv = gl_FragCoord.xy / u_resolution.xy;
15
+ vec2 centered = uv * 2.0 - 1.0;
16
+ centered.x *= u_resolution.x / max(u_resolution.y, 1.0);
17
+
18
+ float intensity = max(u_param_intensity, u_amplitude);
19
+ float bass = max(u_param_bass, u_bass);
20
+ float wave = sin(centered.x * 9.0 + u_time * (2.4 + bass * 7.0));
21
+ float lineDistance = abs(centered.y - wave * 0.28);
22
+ float core = smoothstep(0.045 + intensity * 0.03, 0.0, lineDistance);
23
+ float halo = smoothstep(0.12 + intensity * 0.06, 0.0, lineDistance);
24
+
25
+ vec3 color = vec3(0.02, 0.03, 0.08);
26
+ color += vec3(0.22, 0.68, 0.92) * halo * (0.22 + intensity * 0.45);
27
+ color += vec3(0.50, 0.92, 1.0) * core * (0.55 + intensity * 0.55);
28
+ color += vec3(0.95, 0.28, 0.44) * u_param_flash * 0.18;
29
+
30
+ outColor = vec4(color, 1.0);
31
+ }
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # {{project_name}} transition example.
4
+ Vizcore.define do
5
+ scene :intro do
6
+ layer :background do
7
+ shader :neon_grid
8
+ opacity 0.82
9
+ end
10
+
11
+ layer :geometry do
12
+ type :wireframe_cube
13
+ map fft_spectrum => :deform
14
+ map amplitude => :rotation_speed
15
+ map frequency_band(:high) => :color_shift
16
+ end
17
+ end
18
+
19
+ scene :drop do
20
+ layer :particles do
21
+ type :particle_field
22
+ count 3600
23
+ map amplitude => :speed
24
+ map frequency_band(:low) => :size
25
+ map beat? => :flash
26
+ end
27
+
28
+ layer :title do
29
+ type :text
30
+ content "{{project_name}}"
31
+ font_size 96
32
+ map beat? => :flash
33
+ end
34
+ end
35
+
36
+ transition from: :intro, to: :drop do
37
+ trigger { beat_count >= 64 || frame_count >= 360 }
38
+ effect :crossfade, duration: 1.4
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # {{project_name}} MIDI mapping example.
4
+ Vizcore.define do
5
+ midi :controller, device: :default
6
+
7
+ scene :warmup do
8
+ layer :warm_bg do
9
+ shader :neon_grid
10
+ map frequency_band(:mid) => :intensity
11
+ end
12
+ end
13
+
14
+ scene :impact do
15
+ layer :impact_bg do
16
+ shader :glitch_flash
17
+ map beat? => :flash
18
+ map amplitude => :effect_intensity
19
+ end
20
+ end
21
+
22
+ midi_map note: 36 do
23
+ switch_scene :impact
24
+ end
25
+
26
+ midi_map note: 38 do
27
+ switch_scene :warmup
28
+ end
29
+
30
+ midi_map cc: 1 do |value|
31
+ set :global_intensity, value / 127.0
32
+ end
33
+ end
@@ -0,0 +1,35 @@
1
+ # {{project_name}}
2
+
3
+ This project was generated by `vizcore new`.
4
+
5
+ ## Start
6
+
7
+ ```bash
8
+ vizcore start scenes/basic.rb
9
+ ```
10
+
11
+ ## Included Scenes
12
+
13
+ - `scenes/basic.rb`: Minimal wireframe starter
14
+ - `scenes/intro_drop.rb`: Transition flow with beat trigger
15
+ - `scenes/midi_control.rb`: MIDI note/CC mapping example
16
+ - `scenes/custom_shader.rb`: Custom GLSL + post/VJ effect example
17
+
18
+ ## Custom Shader
19
+
20
+ `scenes/custom_shader.rb` references `shaders/custom_wave.frag`.
21
+ Edit that file and save to see hot-reload updates.
22
+
23
+ ## MIDI
24
+
25
+ List devices:
26
+
27
+ ```bash
28
+ vizcore devices midi
29
+ ```
30
+
31
+ Start MIDI example:
32
+
33
+ ```bash
34
+ vizcore start scenes/midi_control.rb
35
+ ```