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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +544 -9
  3. data/docs/.nojekyll +0 -0
  4. data/docs/assets/site.css +744 -0
  5. data/docs/assets/vizcore-demo.gif +0 -0
  6. data/docs/assets/vizcore-poster.png +0 -0
  7. data/docs/assets/vj-tunnel.js +159 -0
  8. data/docs/index.html +224 -0
  9. data/examples/README.md +59 -0
  10. data/examples/assets/README.md +19 -0
  11. data/examples/audio_inspector.rb +34 -0
  12. data/examples/club_intro_drop.rb +78 -0
  13. data/examples/kansai_rubykaigi_visual.rb +70 -0
  14. data/examples/live_coding_minimal.rb +22 -0
  15. data/examples/midi_controller_show.rb +78 -0
  16. data/examples/midi_scene_switch.rb +3 -1
  17. data/examples/parser_visualizer.rb +48 -0
  18. data/examples/readme_demo.rb +17 -0
  19. data/examples/rhythm_geometry.rb +34 -0
  20. data/examples/ruby_crystal_show.rb +35 -0
  21. data/examples/shader_playground.rb +18 -0
  22. data/examples/unyo_liquid.rb +59 -0
  23. data/examples/vj_ambient_chill_room.rb +124 -0
  24. data/examples/vj_dnb_jungle.rb +170 -0
  25. data/examples/vj_festival_mainstage.rb +245 -0
  26. data/examples/vj_festival_mainstage.yml +17 -0
  27. data/examples/vj_glitch_industrial.rb +164 -0
  28. data/examples/vj_hiphop_cipher.rb +167 -0
  29. data/examples/vj_jpop_idol_live.rb +210 -0
  30. data/examples/vj_synthwave_retro.rb +173 -0
  31. data/examples/vj_techno_warehouse.rb +195 -0
  32. data/frontend/index.html +468 -2
  33. data/frontend/src/audio-inspector.js +40 -0
  34. data/frontend/src/live-controls.js +131 -0
  35. data/frontend/src/main.js +792 -16
  36. data/frontend/src/midi-learn.js +194 -0
  37. data/frontend/src/performance-monitor.js +183 -0
  38. data/frontend/src/plugin-runtime.js +130 -0
  39. data/frontend/src/projector-mode.js +56 -0
  40. data/frontend/src/renderer/engine.js +148 -3
  41. data/frontend/src/renderer/layer-manager.js +428 -30
  42. data/frontend/src/renderer/shader-manager.js +26 -0
  43. data/frontend/src/runtime-control-preset.js +11 -0
  44. data/frontend/src/shader-error-overlay.js +29 -0
  45. data/frontend/src/shader-param-controls.js +93 -0
  46. data/frontend/src/shaders/builtins.js +380 -2
  47. data/frontend/src/shaders/post-effects.js +52 -0
  48. data/frontend/src/visual-regression.js +67 -0
  49. data/frontend/src/visual-settings-preset.js +103 -0
  50. data/frontend/src/visuals/geometry.js +268 -0
  51. data/frontend/src/visuals/image-renderer.js +291 -0
  52. data/frontend/src/visuals/particle-system.js +56 -10
  53. data/frontend/src/visuals/spectrogram-renderer.js +226 -0
  54. data/frontend/src/visuals/text-renderer.js +112 -11
  55. data/frontend/src/websocket-client.js +12 -1
  56. data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
  57. data/lib/vizcore/analysis/beat_detector.rb +4 -2
  58. data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
  59. data/lib/vizcore/analysis/feature_recorder.rb +159 -0
  60. data/lib/vizcore/analysis/feature_replay.rb +84 -0
  61. data/lib/vizcore/analysis/pipeline.rb +235 -11
  62. data/lib/vizcore/analysis/tap_tempo.rb +74 -0
  63. data/lib/vizcore/analysis.rb +4 -0
  64. data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
  65. data/lib/vizcore/audio/fixture_input.rb +65 -0
  66. data/lib/vizcore/audio/input_manager.rb +4 -2
  67. data/lib/vizcore/audio/mic_input.rb +24 -8
  68. data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
  69. data/lib/vizcore/audio.rb +1 -0
  70. data/lib/vizcore/cli/doctor.rb +159 -0
  71. data/lib/vizcore/cli/dsl_reference.rb +99 -0
  72. data/lib/vizcore/cli/layer_docs.rb +46 -0
  73. data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
  74. data/lib/vizcore/cli/scene_inspector.rb +136 -0
  75. data/lib/vizcore/cli/scene_validator.rb +245 -0
  76. data/lib/vizcore/cli/shader_template.rb +68 -0
  77. data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
  78. data/lib/vizcore/cli.rb +689 -18
  79. data/lib/vizcore/config.rb +103 -2
  80. data/lib/vizcore/control_preset.rb +68 -0
  81. data/lib/vizcore/dsl/engine.rb +277 -5
  82. data/lib/vizcore/dsl/layer_builder.rb +491 -22
  83. data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
  84. data/lib/vizcore/dsl/mapping_resolver.rb +132 -3
  85. data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
  86. data/lib/vizcore/dsl/reaction_builder.rb +44 -0
  87. data/lib/vizcore/dsl/scene_builder.rb +61 -5
  88. data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
  89. data/lib/vizcore/dsl/style_builder.rb +68 -0
  90. data/lib/vizcore/dsl/timeline_builder.rb +138 -0
  91. data/lib/vizcore/dsl/transition_controller.rb +77 -0
  92. data/lib/vizcore/dsl.rb +5 -1
  93. data/lib/vizcore/layer_catalog.rb +273 -0
  94. data/lib/vizcore/project_manifest.rb +152 -0
  95. data/lib/vizcore/renderer/png_writer.rb +57 -0
  96. data/lib/vizcore/renderer/render_sequence.rb +153 -0
  97. data/lib/vizcore/renderer/scene_frame_source.rb +119 -0
  98. data/lib/vizcore/renderer/scene_serializer.rb +36 -3
  99. data/lib/vizcore/renderer/snapshot.rb +38 -0
  100. data/lib/vizcore/renderer/snapshot_renderer.rb +446 -0
  101. data/lib/vizcore/renderer.rb +5 -0
  102. data/lib/vizcore/server/frame_broadcaster.rb +91 -5
  103. data/lib/vizcore/server/gallery_app.rb +155 -0
  104. data/lib/vizcore/server/gallery_page.rb +100 -0
  105. data/lib/vizcore/server/gallery_runner.rb +48 -0
  106. data/lib/vizcore/server/rack_app.rb +203 -4
  107. data/lib/vizcore/server/runner.rb +370 -22
  108. data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
  109. data/lib/vizcore/server/websocket_handler.rb +60 -10
  110. data/lib/vizcore/server.rb +4 -0
  111. data/lib/vizcore/sync/osc_message.rb +103 -0
  112. data/lib/vizcore/sync/osc_receiver.rb +68 -0
  113. data/lib/vizcore/sync.rb +4 -0
  114. data/lib/vizcore/templates/midi_control_scene.rb +3 -1
  115. data/lib/vizcore/templates/plugin_layer.rb +20 -0
  116. data/lib/vizcore/templates/plugin_readme.md +23 -0
  117. data/lib/vizcore/templates/plugin_renderer.js +43 -0
  118. data/lib/vizcore/templates/plugin_scene.rb +14 -0
  119. data/lib/vizcore/templates/project_readme.md +7 -23
  120. data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
  121. data/lib/vizcore/version.rb +1 -1
  122. data/lib/vizcore.rb +27 -0
  123. data/scripts/browser_capture.mjs +75 -0
  124. data/sig/vizcore.rbs +362 -0
  125. metadata +83 -3
  126. data/docs/GETTING_STARTED.md +0 -105
@@ -9,6 +9,10 @@ module Vizcore
9
9
  module Server
10
10
  # Stateless WebSocket endpoint manager for frame broadcast transport.
11
11
  class WebSocketHandler
12
+ PROTOCOL_VERSION = "vizcore.frame.v1"
13
+ MAX_BUFFERED_FRAME_BYTES = 1_000_000
14
+ DROPPABLE_MESSAGE_TYPES = Set["audio_frame"].freeze
15
+
12
16
  class << self
13
17
  # Rack endpoint for WebSocket upgrade handling.
14
18
  #
@@ -36,15 +40,29 @@ module Vizcore
36
40
  def broadcast(type:, payload:)
37
41
  return false unless faye_websocket_class
38
42
 
39
- message = JSON.generate(type: type, payload: payload)
43
+ message = JSON.generate(protocol: PROTOCOL_VERSION, type: type, payload: payload)
40
44
 
41
45
  each_socket do |socket|
42
- send_message(socket, message)
46
+ send_message(socket, message, type: type)
43
47
  end
44
48
 
45
49
  true
46
50
  end
47
51
 
52
+ # Send one typed payload to a single websocket client.
53
+ #
54
+ # @param socket [#send]
55
+ # @param type [String]
56
+ # @param payload [Hash]
57
+ # @return [Boolean] false when websocket backend is unavailable
58
+ def send_to(socket, type:, payload:)
59
+ return false unless faye_websocket_class
60
+
61
+ message = JSON.generate(protocol: PROTOCOL_VERSION, type: type, payload: payload)
62
+ send_message(socket, message, type: type)
63
+ true
64
+ end
65
+
48
66
  # @return [Integer]
49
67
  def connection_count
50
68
  mutex.synchronize { sockets.size }
@@ -55,6 +73,11 @@ module Vizcore
55
73
  mutex.synchronize { @last_error }
56
74
  end
57
75
 
76
+ # @return [Integer]
77
+ def dropped_frame_count
78
+ mutex.synchronize { @dropped_frame_count || 0 }
79
+ end
80
+
58
81
  # Register one inbound message handler for client -> server control messages.
59
82
  #
60
83
  # @yieldparam message [Hash]
@@ -79,21 +102,44 @@ module Vizcore
79
102
  nil
80
103
  end
81
104
 
82
- def send_message(socket, message)
105
+ def send_message(socket, message, type:)
106
+ return if drop_for_backpressure?(socket, type)
107
+
83
108
  if event_machine_reactor_running?
84
- EventMachine.schedule { safe_send(socket, message) }
109
+ EventMachine.schedule { safe_send(socket, message, type: type) }
85
110
  else
86
- safe_send(socket, message)
111
+ safe_send(socket, message, type: type)
87
112
  end
88
113
  end
89
114
 
90
- def safe_send(socket, message)
115
+ def safe_send(socket, message, type:)
116
+ return if drop_for_backpressure?(socket, type)
117
+
91
118
  socket.send(message)
92
119
  rescue StandardError => e
93
120
  set_last_error(e)
94
121
  unregister(socket)
95
122
  end
96
123
 
124
+ def drop_for_backpressure?(socket, type)
125
+ return false unless DROPPABLE_MESSAGE_TYPES.include?(type.to_s)
126
+
127
+ buffered_amount = socket_buffered_amount(socket)
128
+ return false unless buffered_amount && buffered_amount > MAX_BUFFERED_FRAME_BYTES
129
+
130
+ increment_dropped_frame_count
131
+ true
132
+ end
133
+
134
+ def socket_buffered_amount(socket)
135
+ return socket.buffered_amount if socket.respond_to?(:buffered_amount)
136
+ return socket.bufferedAmount if socket.respond_to?(:bufferedAmount)
137
+
138
+ nil
139
+ rescue StandardError
140
+ nil
141
+ end
142
+
97
143
  def event_machine_reactor_running?
98
144
  require "eventmachine"
99
145
  EventMachine.reactor_running?
@@ -105,9 +151,9 @@ module Vizcore
105
151
  [500, json_headers, [JSON.generate(error: "Missing dependency: faye-websocket")]]
106
152
  end
107
153
 
108
- def handle_message(_socket, raw_message)
154
+ def handle_message(socket, raw_message)
109
155
  message = JSON.parse(raw_message)
110
- dispatch_message(message)
156
+ dispatch_message(message, socket)
111
157
  message
112
158
  rescue JSON::ParserError => e
113
159
  set_last_error(e)
@@ -139,12 +185,16 @@ module Vizcore
139
185
  mutex.synchronize { @last_error = error }
140
186
  end
141
187
 
142
- def dispatch_message(message)
188
+ def increment_dropped_frame_count
189
+ mutex.synchronize { @dropped_frame_count = (@dropped_frame_count || 0) + 1 }
190
+ end
191
+
192
+ def dispatch_message(message, socket)
143
193
  handler = mutex.synchronize { @message_handler }
144
194
  return unless handler
145
195
  return unless message.is_a?(Hash)
146
196
 
147
- handler.call(message)
197
+ handler.call(message, socket)
148
198
  rescue StandardError => e
149
199
  set_last_error(e)
150
200
  nil
@@ -7,6 +7,10 @@ module Vizcore
7
7
  end
8
8
 
9
9
  require_relative "server/frame_broadcaster"
10
+ require_relative "server/gallery_page"
11
+ require_relative "server/gallery_app"
12
+ require_relative "server/gallery_runner"
10
13
  require_relative "server/rack_app"
14
+ require_relative "server/scene_dependency_watcher"
11
15
  require_relative "server/runner"
12
16
  require_relative "server/websocket_handler"
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ module Sync
5
+ # Minimal OSC 1.0 message parser for control sync.
6
+ class OscMessage
7
+ attr_reader :address, :arguments
8
+
9
+ # @param data [String]
10
+ # @return [Vizcore::Sync::OscMessage, nil]
11
+ def self.parse(data)
12
+ parser = Parser.new(data)
13
+ address = parser.read_string
14
+ return nil unless address&.start_with?("/")
15
+
16
+ tags = parser.read_string
17
+ arguments = parser.read_arguments(tags)
18
+ new(address: address, arguments: arguments)
19
+ rescue StandardError
20
+ nil
21
+ end
22
+
23
+ # @param address [String]
24
+ # @param arguments [Array]
25
+ def initialize(address:, arguments: [])
26
+ @address = address
27
+ @arguments = Array(arguments)
28
+ end
29
+
30
+ # @api private
31
+ class Parser
32
+ # @param data [String]
33
+ def initialize(data)
34
+ @data = data.to_s.b
35
+ @offset = 0
36
+ end
37
+
38
+ # @return [String, nil]
39
+ def read_string
40
+ start = @offset
41
+ @offset += 1 while @offset < @data.bytesize && @data.getbyte(@offset) != 0
42
+ return nil if @offset >= @data.bytesize
43
+
44
+ value = @data.byteslice(start...@offset).to_s.force_encoding(Encoding::UTF_8)
45
+ @offset += 1
46
+ align_offset
47
+ value
48
+ end
49
+
50
+ # @param tags [String, nil]
51
+ # @return [Array]
52
+ def read_arguments(tags)
53
+ return [] if tags.to_s.empty?
54
+ return [] unless tags.start_with?(",")
55
+
56
+ tags[1..].to_s.each_char.map do |tag|
57
+ read_argument(tag)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def read_argument(tag)
64
+ case tag
65
+ when "i"
66
+ read_int32
67
+ when "f"
68
+ read_float32
69
+ when "s"
70
+ read_string
71
+ when "T"
72
+ true
73
+ when "F"
74
+ false
75
+ else
76
+ nil
77
+ end
78
+ end
79
+
80
+ def read_int32
81
+ value = read_bytes(4).unpack1("N")
82
+ value >= 0x80000000 ? value - 0x100000000 : value
83
+ end
84
+
85
+ def read_float32
86
+ read_bytes(4).unpack1("g")
87
+ end
88
+
89
+ def read_bytes(length)
90
+ raise ArgumentError, "OSC payload truncated" if @offset + length > @data.bytesize
91
+
92
+ @data.byteslice(@offset, length).tap do
93
+ @offset += length
94
+ end
95
+ end
96
+
97
+ def align_offset
98
+ @offset += 1 while (@offset % 4).positive?
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require_relative "osc_message"
5
+
6
+ module Vizcore
7
+ module Sync
8
+ # Receives OSC UDP messages on a background thread.
9
+ class OscReceiver
10
+ DEFAULT_HOST = "127.0.0.1"
11
+ MAX_PACKET_SIZE = 65_536
12
+
13
+ # @param port [Integer]
14
+ # @param host [String]
15
+ # @param handler [#call]
16
+ # @param error_reporter [#call, nil]
17
+ def initialize(port:, host: DEFAULT_HOST, handler:, error_reporter: nil)
18
+ @host = host.to_s
19
+ @port = Integer(port)
20
+ @handler = handler
21
+ @error_reporter = error_reporter
22
+ @socket = nil
23
+ @thread = nil
24
+ @running = false
25
+ end
26
+
27
+ # @return [Vizcore::Sync::OscReceiver]
28
+ def start
29
+ return self if @thread&.alive?
30
+
31
+ @socket = UDPSocket.new
32
+ @socket.bind(@host, @port)
33
+ @running = true
34
+ @thread = Thread.new { receive_loop }
35
+ self
36
+ end
37
+
38
+ # @return [nil]
39
+ def stop
40
+ @running = false
41
+ @socket&.close
42
+ @thread&.join(1)
43
+ nil
44
+ rescue StandardError
45
+ nil
46
+ ensure
47
+ @socket = nil
48
+ @thread = nil
49
+ end
50
+
51
+ private
52
+
53
+ def receive_loop
54
+ while @running
55
+ begin
56
+ data, = @socket.recvfrom(MAX_PACKET_SIZE)
57
+ message = OscMessage.parse(data)
58
+ @handler.call(message) if message
59
+ rescue IOError, SystemCallError
60
+ break unless @running
61
+ rescue StandardError => e
62
+ @error_reporter&.call("OSC message ignored: #{e.message}")
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sync/osc_message"
4
+ require_relative "sync/osc_receiver"
@@ -2,11 +2,13 @@
2
2
 
3
3
  # {{project_name}} MIDI mapping example.
4
4
  Vizcore.define do
5
+ set :global_intensity, 0.65
6
+
5
7
  midi :controller, device: :default
6
8
 
7
9
  scene :warmup do
8
10
  layer :warm_bg do
9
- shader :neon_grid
11
+ shader :waveform_ribbon
10
12
  map frequency_band(:mid) => :intensity
11
13
  end
12
14
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "vizcore"
4
+
5
+ module {{plugin_module}}
6
+ LAYER_TYPE = :{{plugin_type}}
7
+
8
+ Vizcore.register_layer_capability(
9
+ type: LAYER_TYPE,
10
+ aliases: [:{{plugin_name}}],
11
+ params: {
12
+ intensity: "Float",
13
+ color: "CSS color",
14
+ blend: "Blend mode",
15
+ opacity: "Float"
16
+ },
17
+ mappable_params: %i[intensity opacity],
18
+ description: "{{plugin_title}} browser-rendered plugin layer."
19
+ )
20
+ end
@@ -0,0 +1,23 @@
1
+ # {{plugin_title}}
2
+
3
+ Vizcore plugin scaffold generated by `vizcore plugin new`.
4
+
5
+ ## Files
6
+
7
+ - `lib/{{plugin_name}}.rb` registers the Ruby layer capability used by validation and docs.
8
+ - `frontend/{{plugin_name}}-renderer.js` registers a browser renderer through `globalThis.VizcorePlugins`.
9
+ - `examples/{{plugin_name}}_scene.rb` shows the layer type in a scene.
10
+
11
+ ## Try It
12
+
13
+ ```ruby
14
+ require_relative "lib/{{plugin_name}}"
15
+ ```
16
+
17
+ Serve `frontend/{{plugin_name}}-renderer.js` with your Vizcore frontend and load
18
+ it after the built-in app script. The browser runtime exposes
19
+ `globalThis.VizcorePlugins.apiVersion`; this scaffold expects version 1 or
20
+ newer. The browser renderer returns line points, so Vizcore can composite it
21
+ with normal layer `blend` and `opacity` params. Shader plugins can instead
22
+ register with `registerShaderRenderer(type, renderer)` and return a GLSL
23
+ fragment shader string or `{ kind: "shader", fragmentShader }`.
@@ -0,0 +1,43 @@
1
+ const layerType = "{{plugin_type}}";
2
+
3
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
4
+
5
+ export const {{plugin_renderer}} = ({ audio, time, layer }) => {
6
+ const params = layer?.params || {};
7
+ const amplitude = clamp(Number(audio?.amplitude || 0), 0, 1);
8
+ const intensity = clamp(Number(params.intensity ?? 1), 0, 4);
9
+ const radius = 0.25 + amplitude * intensity * 0.18;
10
+ const phase = Number(time || 0) * 0.8;
11
+ const points = [];
12
+
13
+ for (let index = 0; index < 18; index += 1) {
14
+ const angle = phase + index * Math.PI / 9;
15
+ const nextAngle = angle + Math.PI * 0.35;
16
+ points.push(
17
+ Math.cos(angle) * radius,
18
+ Math.sin(angle) * radius,
19
+ Math.cos(nextAngle) * (radius + 0.18),
20
+ Math.sin(nextAngle) * (radius + 0.18),
21
+ );
22
+ }
23
+
24
+ return {
25
+ kind: "lines",
26
+ color: [0.45 + amplitude * 0.35, 0.9, 1.0],
27
+ points,
28
+ };
29
+ };
30
+
31
+ const register = () => {
32
+ const runtime = globalThis.VizcorePlugins;
33
+ if (!runtime?.registerLayerRenderer || Number(runtime.apiVersion || 0) < 1) {
34
+ return false;
35
+ }
36
+
37
+ runtime.registerLayerRenderer(layerType, {{plugin_renderer}});
38
+ return true;
39
+ };
40
+
41
+ if (!register() && typeof globalThis.addEventListener === "function") {
42
+ globalThis.addEventListener("vizcore:plugins-ready", register, { once: true });
43
+ }
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/{{plugin_name}}"
4
+
5
+ Vizcore.define do
6
+ scene :{{plugin_name}}_demo do
7
+ layer :{{plugin_name}} do
8
+ type :{{plugin_type}}
9
+ intensity 1.0
10
+ fill "#67e8f9"
11
+ blend :add
12
+ end
13
+ end
14
+ end
@@ -2,34 +2,18 @@
2
2
 
3
3
  This project was generated by `vizcore new`.
4
4
 
5
+ Template: `{{template_name}}`
6
+
5
7
  ## Start
6
8
 
7
9
  ```bash
8
- vizcore start scenes/basic.rb
10
+ vizcore start {{start_scene}}
9
11
  ```
10
12
 
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
13
+ ## Included Files
24
14
 
25
- List devices:
15
+ {{included_files}}
26
16
 
27
- ```bash
28
- vizcore devices midi
29
- ```
30
-
31
- Start MIDI example:
17
+ ## Notes
32
18
 
33
- ```bash
34
- vizcore start scenes/midi_control.rb
35
- ```
19
+ {{template_notes}}
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # {{project_name}} Ruby conference visual starter.
4
+ Vizcore.define do
5
+ scene :rubykaigi do
6
+ layer :ruby_grid do
7
+ shader :neon_grid
8
+ opacity 0.72
9
+ blend :screen
10
+ map frequency_band(:mid) => :intensity
11
+ end
12
+
13
+ layer :ruby_pulse do
14
+ type :wireframe_cube
15
+ blend :add
16
+ map amplitude => :rotation_speed, range: 0.5..3.2
17
+ map fft_spectrum => :deform
18
+ map frequency_band(:high) => :color_shift
19
+ end
20
+
21
+ layer :title do
22
+ type :text
23
+ content "{{project_name}}"
24
+ font_size 88
25
+ color "#e11d48"
26
+ glow_strength 0.35
27
+ map beat? => :flash
28
+ end
29
+ end
30
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Vizcore
4
4
  # Current gem version.
5
- VERSION = "0.1.0"
5
+ VERSION = "1.0.0"
6
6
  end
data/lib/vizcore.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "vizcore/version"
4
4
  require_relative "vizcore/errors"
5
+ require_relative "vizcore/layer_catalog"
5
6
  require_relative "vizcore/dsl"
6
7
  require "pathname"
7
8
 
@@ -34,4 +35,30 @@ module Vizcore
34
35
  def self.define(&block)
35
36
  DSL::Engine.define(&block)
36
37
  end
38
+
39
+ # Load a Vizcore plugin by Ruby require path.
40
+ #
41
+ # @param name [String, Symbol] require path for the plugin
42
+ # @return [true]
43
+ def self.plugin(name)
44
+ require name.to_s
45
+ end
46
+
47
+ # Register a plugin-provided layer capability for validation and docs.
48
+ #
49
+ # @param type [Symbol, String] primary layer type
50
+ # @param aliases [Array<Symbol, String>] supported aliases
51
+ # @param params [Hash] layer parameter metadata
52
+ # @param mappable_params [Array<Symbol, String>] params that can be mapped
53
+ # @param description [String, nil] human-readable docs text
54
+ # @return [Vizcore::LayerCatalog::Capability]
55
+ def self.register_layer_capability(type:, aliases: [], params: {}, mappable_params: [], description: nil)
56
+ LayerCatalog.register_layer_capability(
57
+ type: type,
58
+ aliases: aliases,
59
+ params: params,
60
+ mappable_params: mappable_params,
61
+ description: description
62
+ )
63
+ end
37
64
  end
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const options = parseArgs(process.argv.slice(2));
5
+
6
+ if (!options.url) {
7
+ console.error("Usage: node scripts/browser_capture.mjs URL --out browser-capture.png [--selector #vizcore-canvas] [--wait 1000]");
8
+ process.exit(1);
9
+ }
10
+
11
+ let chromium;
12
+ try {
13
+ ({ chromium } = await import("playwright"));
14
+ } catch {
15
+ console.error("Playwright is required for browser capture. Install it with `npm install -D playwright` in the frontend project.");
16
+ process.exit(2);
17
+ }
18
+
19
+ const browser = await chromium.launch({ headless: true });
20
+ try {
21
+ const page = await browser.newPage({ viewport: { width: options.width, height: options.height } });
22
+ await page.goto(options.url, { waitUntil: "domcontentloaded" });
23
+ if (options.wait > 0) {
24
+ await page.waitForTimeout(options.wait);
25
+ }
26
+
27
+ const element = page.locator(options.selector).first();
28
+ await element.waitFor({ state: "visible", timeout: 10000 });
29
+ await fs.mkdir(path.dirname(options.out), { recursive: true });
30
+ await element.screenshot({ path: options.out });
31
+ console.log(`Browser capture written: ${options.out}`);
32
+ } finally {
33
+ await browser.close();
34
+ }
35
+
36
+ function parseArgs(args) {
37
+ const parsed = {
38
+ url: null,
39
+ out: "browser-capture.png",
40
+ selector: "#vizcore-canvas",
41
+ wait: 1000,
42
+ width: 1280,
43
+ height: 720,
44
+ };
45
+
46
+ for (let index = 0; index < args.length; index += 1) {
47
+ const value = args[index];
48
+ if (value === "--out") {
49
+ parsed.out = String(args[index + 1] || parsed.out);
50
+ index += 1;
51
+ } else if (value === "--selector") {
52
+ parsed.selector = String(args[index + 1] || parsed.selector);
53
+ index += 1;
54
+ } else if (value === "--wait") {
55
+ parsed.wait = finiteNumber(args[index + 1], parsed.wait);
56
+ index += 1;
57
+ } else if (value === "--width") {
58
+ parsed.width = finiteNumber(args[index + 1], parsed.width);
59
+ index += 1;
60
+ } else if (value === "--height") {
61
+ parsed.height = finiteNumber(args[index + 1], parsed.height);
62
+ index += 1;
63
+ } else if (!parsed.url) {
64
+ parsed.url = value;
65
+ }
66
+ }
67
+
68
+ parsed.out = path.resolve(parsed.out);
69
+ return parsed;
70
+ }
71
+
72
+ function finiteNumber(value, fallback) {
73
+ const numeric = Number(value);
74
+ return Number.isFinite(numeric) ? numeric : fallback;
75
+ }