vizcore 1.1.0 → 1.2.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/index.html +24 -2
  3. data/frontend/src/audio-inspector.js +9 -0
  4. data/frontend/src/live-controls.js +219 -7
  5. data/frontend/src/main.js +447 -57
  6. data/frontend/src/midi-learn.js +22 -2
  7. data/frontend/src/performance-monitor.js +137 -1
  8. data/frontend/src/renderer/engine.js +391 -10
  9. data/frontend/src/renderer/layer-manager.js +472 -71
  10. data/frontend/src/runtime-control-preset.js +44 -0
  11. data/frontend/src/scene-patches.js +159 -0
  12. data/frontend/src/shader-error-overlay.js +1 -0
  13. data/frontend/src/visuals/image-renderer.js +19 -0
  14. data/frontend/src/visuals/particle-system.js +10 -0
  15. data/frontend/src/visuals/shape-renderer.js +13 -0
  16. data/frontend/src/visuals/spectrogram-renderer.js +14 -0
  17. data/frontend/src/visuals/text-renderer.js +13 -0
  18. data/frontend/src/websocket-client.js +6 -0
  19. data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
  20. data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
  21. data/lib/vizcore/analysis/feature_recorder.rb +117 -7
  22. data/lib/vizcore/analysis/feature_replay.rb +48 -9
  23. data/lib/vizcore/analysis/pipeline.rb +258 -9
  24. data/lib/vizcore/analysis/tap_tempo.rb +17 -2
  25. data/lib/vizcore/audio/calibration.rb +156 -0
  26. data/lib/vizcore/audio/file_input.rb +28 -0
  27. data/lib/vizcore/audio/input_manager.rb +36 -1
  28. data/lib/vizcore/audio/midi_input.rb +5 -0
  29. data/lib/vizcore/audio/ring_buffer.rb +22 -0
  30. data/lib/vizcore/audio.rb +1 -0
  31. data/lib/vizcore/cli/dsl_reference.rb +64 -8
  32. data/lib/vizcore/cli/plugin_checker.rb +93 -0
  33. data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
  34. data/lib/vizcore/cli/scene_inspector.rb +35 -1
  35. data/lib/vizcore/cli/scene_validator.rb +487 -39
  36. data/lib/vizcore/cli/shader_template.rb +7 -2
  37. data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
  38. data/lib/vizcore/cli.rb +268 -15
  39. data/lib/vizcore/config.rb +40 -3
  40. data/lib/vizcore/control_preset.rb +29 -0
  41. data/lib/vizcore/deep_copy.rb +21 -0
  42. data/lib/vizcore/dsl/color_helpers.rb +155 -0
  43. data/lib/vizcore/dsl/engine.rb +219 -23
  44. data/lib/vizcore/dsl/layer_builder.rb +278 -15
  45. data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
  46. data/lib/vizcore/dsl/layout_helpers.rb +290 -0
  47. data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
  48. data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
  49. data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
  50. data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
  51. data/lib/vizcore/dsl/reaction_builder.rb +1 -0
  52. data/lib/vizcore/dsl/scene_builder.rb +83 -13
  53. data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
  54. data/lib/vizcore/dsl/style_builder.rb +3 -0
  55. data/lib/vizcore/dsl/timeline_builder.rb +91 -8
  56. data/lib/vizcore/dsl/transition_controller.rb +157 -18
  57. data/lib/vizcore/dsl.rb +2 -0
  58. data/lib/vizcore/layer_catalog.rb +1 -0
  59. data/lib/vizcore/plugin_asset_policy.rb +55 -0
  60. data/lib/vizcore/project_manifest.rb +12 -2
  61. data/lib/vizcore/renderer/render_sequence.rb +104 -13
  62. data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
  63. data/lib/vizcore/renderer/scene_serializer.rb +38 -0
  64. data/lib/vizcore/renderer/snapshot.rb +4 -3
  65. data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
  66. data/lib/vizcore/scene_trust.rb +31 -0
  67. data/lib/vizcore/server/frame_broadcaster.rb +469 -23
  68. data/lib/vizcore/server/rack_app.rb +151 -4
  69. data/lib/vizcore/server/runner.rb +676 -82
  70. data/lib/vizcore/server/websocket_handler.rb +236 -14
  71. data/lib/vizcore/server.rb +21 -0
  72. data/lib/vizcore/shape.rb +39 -16
  73. data/lib/vizcore/sync/osc_message.rb +66 -9
  74. data/lib/vizcore/version.rb +1 -1
  75. data/lib/vizcore.rb +33 -0
  76. data/scripts/browser_capture.mjs +31 -2
  77. data/sig/vizcore.rbs +55 -4
  78. metadata +18 -3
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vizcore
4
+ module DSL
5
+ # Reusable layout helper methods for DSL shape point generation.
6
+ module LayoutHelpers
7
+ # Generate a rectangular grid of points.
8
+ #
9
+ # @param count [Integer] total number of points to generate
10
+ # @param columns [Integer, nil] optional grid column count
11
+ # @param rows [Integer, nil] optional grid row count
12
+ # @param spacing [Numeric] spacing used for x/y when axis spacing is omitted
13
+ # @param spacing_x [Numeric, nil] optional override for x spacing
14
+ # @param spacing_y [Numeric, nil] optional override for y spacing
15
+ # @param center [Boolean] whether to center the grid around the origin
16
+ # @param origin [Array<Numeric>] origin point [x, y]
17
+ # @return [Array<Array<Float>>]
18
+ def grid(count:, columns: nil, rows: nil, spacing: 1.0, spacing_x: nil, spacing_y: nil, center: true, origin: [0, 0])
19
+ point_count = normalize_positive_integer(count, :count)
20
+
21
+ columns = normalize_positive_integer(columns, :columns) if columns
22
+ rows = normalize_positive_integer(rows, :rows) if rows
23
+
24
+ if columns.nil? && rows.nil?
25
+ raise ArgumentError, "grid requires columns or rows"
26
+ end
27
+
28
+ columns ||= Float(point_count) / rows.to_f
29
+ rows ||= Float(point_count) / columns.to_f
30
+
31
+ columns = columns.ceil
32
+ rows = rows.ceil
33
+
34
+ if columns <= 0 || rows <= 0
35
+ raise ArgumentError, "grid columns and rows must be positive"
36
+ end
37
+
38
+ x_step = normalize_positive_number(spacing_x || spacing, :spacing_x)
39
+ y_step = normalize_positive_number(spacing_y || spacing, :spacing_y)
40
+
41
+ origin_x, origin_y = normalize_xy_pair(origin, :origin)
42
+
43
+ x_offset = center && columns > 1 ? -x_step * (columns - 1) / 2.0 : 0.0
44
+ y_offset = center && rows > 1 ? -y_step * (rows - 1) / 2.0 : 0.0
45
+
46
+ points = []
47
+ rows.times do |row|
48
+ columns.times do |column|
49
+ break if points.length >= point_count
50
+
51
+ points << [
52
+ origin_x + x_offset + (column * x_step),
53
+ origin_y + y_offset + (row * y_step)
54
+ ]
55
+ end
56
+ end
57
+
58
+ points
59
+ end
60
+
61
+ # Generate points evenly distributed around a circle.
62
+ #
63
+ # @param count [Integer] number of points
64
+ # @param radius [Numeric] circle radius
65
+ # @param start_angle [Numeric] start angle in degrees
66
+ # @param span [Numeric] angular span in degrees
67
+ # @param radius_jitter [Numeric] random jitter applied per-point
68
+ # @param seed [Integer, nil] deterministic jitter seed
69
+ # @param origin [Array<Numeric>] origin point [x, y]
70
+ # @return [Array<Array<Float>>]
71
+ def radial(count:, radius:, start_angle: -90.0, span: 360.0, radius_jitter: 0.0, seed: nil, origin: [0, 0])
72
+ point_count = normalize_positive_integer(count, :count)
73
+ radius = normalize_non_negative_number(radius, :radius)
74
+ span = Float(span)
75
+ start = degrees_to_radians(start_angle)
76
+ jitter = normalize_non_negative_number(radius_jitter, :radius_jitter)
77
+ random = Random.new(Integer(seed || 0))
78
+
79
+ origin_x, origin_y = normalize_xy_pair(origin, :origin)
80
+
81
+ points = []
82
+ point_count.times do |index|
83
+ ratio = point_count == 1 ? 0.0 : index.to_f / point_count.to_f
84
+ angle = start + (ratio * span) * Math::PI / 180.0
85
+ jitter_amount = jitter.zero? ? 0.0 : (random.rand * 2.0 - 1.0) * jitter
86
+ scaled_radius = radius + jitter_amount
87
+ points << [
88
+ normalize_layout_coordinate(origin_x + Math.cos(angle) * scaled_radius),
89
+ normalize_layout_coordinate(origin_y + Math.sin(angle) * scaled_radius)
90
+ ]
91
+ end
92
+
93
+ points
94
+ end
95
+
96
+ # Generate spiral points from center outward.
97
+ #
98
+ # @param count [Integer] number of points
99
+ # @param radius [Numeric] outer radius
100
+ # @param turns [Numeric] number of turns
101
+ # @param start_radius [Numeric] inner radius
102
+ # @param start_angle [Numeric] start angle in degrees
103
+ # @param origin [Array<Numeric>] origin point [x, y]
104
+ # @return [Array<Array<Float>>]
105
+ def spiral(count:, radius:, turns: 2.0, start_radius: 0.0, start_angle: -90.0, origin: [0, 0])
106
+ point_count = normalize_positive_integer(count, :count)
107
+ outer_radius = normalize_non_negative_number(radius, :radius)
108
+ start_r = normalize_non_negative_number(start_radius, :start_radius)
109
+ turns = Float(turns)
110
+ start = degrees_to_radians(start_angle)
111
+
112
+ if outer_radius < start_r
113
+ raise ArgumentError, "spiral radius must be greater than or equal to start_radius"
114
+ end
115
+
116
+ radius_delta = outer_radius - start_r
117
+ full_turns = turns
118
+ origin_x, origin_y = normalize_xy_pair(origin, :origin)
119
+
120
+ points = []
121
+ point_count.times do |index|
122
+ ratio = point_count == 1 ? 0.0 : index.to_f / (point_count - 1).to_f
123
+ current_radius = start_r + (radius_delta * ratio)
124
+ angle = start + ratio * full_turns * Math::PI * 2.0
125
+ points << [
126
+ normalize_layout_coordinate(origin_x + Math.cos(angle) * current_radius),
127
+ normalize_layout_coordinate(origin_y + Math.sin(angle) * current_radius)
128
+ ]
129
+ end
130
+
131
+ points
132
+ end
133
+
134
+ # Generate points packed in concentric rings up to target count.
135
+ #
136
+ # @param count [Integer] target point count
137
+ # @param radius [Numeric] outer packing radius
138
+ # @param min_distance [Numeric, nil] approximate distance between neighboring points
139
+ # @param origin [Array<Numeric>] origin point [x, y]
140
+ # @return [Array<Array<Float>>]
141
+ def circle_pack(count:, radius:, min_distance: nil, origin: [0, 0])
142
+ target_count = normalize_positive_integer(count, :count)
143
+ outer_radius = normalize_non_negative_number(radius, :radius)
144
+ if target_count == 1
145
+ return [normalize_xy_pair(origin, :origin)]
146
+ end
147
+
148
+ min_distance = if min_distance.nil?
149
+ outer_radius.to_f / Math.sqrt(target_count)
150
+ else
151
+ normalize_positive_number(min_distance, :min_distance)
152
+ end
153
+
154
+ origin_x, origin_y = normalize_xy_pair(origin, :origin)
155
+
156
+ points = [ [origin_x, origin_y] ]
157
+ ring = 1
158
+ while points.length < target_count
159
+ ring_radius = min_distance * ring
160
+ break if ring_radius > outer_radius
161
+
162
+ ring_capacity = [[(2.0 * Math::PI * ring_radius / min_distance).round, 6].max, 1].max
163
+ per_ring = [ring_capacity, target_count - points.length].min
164
+ angle_step = (Math::PI * 2.0) / per_ring
165
+ 0.upto(per_ring - 1) do |index|
166
+ angle = index * angle_step
167
+ points << [
168
+ normalize_layout_coordinate(origin_x + Math.cos(angle) * ring_radius),
169
+ normalize_layout_coordinate(origin_y + Math.sin(angle) * ring_radius)
170
+ ]
171
+ break if points.length >= target_count
172
+ end
173
+
174
+ ring += 1
175
+ end
176
+
177
+ return points if points.length >= target_count
178
+ raise ArgumentError, "circle_pack cannot place #{target_count} points within radius #{outer_radius}"
179
+ end
180
+
181
+ # Generate pseudo-random points inside a box or circle.
182
+ #
183
+ # @param count [Integer] number of points
184
+ # @param width [Numeric, nil] box width
185
+ # @param height [Numeric, nil] box height
186
+ # @param radius [Numeric, nil] circular radius alternative to width/height
187
+ # @param seed [Integer, nil] deterministic random seed
188
+ # @param origin [Array<Numeric>] origin point [x, y]
189
+ # @return [Array<Array<Float>>]
190
+ def scatter(count:, width: nil, height: nil, radius: nil, seed: 0, origin: [0, 0])
191
+ point_count = normalize_positive_integer(count, :count)
192
+
193
+ origin_x, origin_y = normalize_xy_pair(origin, :origin)
194
+ random = Random.new(Integer(seed))
195
+
196
+ if width.nil? && height.nil? && radius.nil?
197
+ width = 100.0
198
+ height = 100.0
199
+ end
200
+
201
+ if radius && !width && !height
202
+ scatter_radius = normalize_non_negative_number(radius, :radius)
203
+ if scatter_radius.zero?
204
+ return Array.new(point_count) { [origin_x, origin_y] }
205
+ end
206
+ elsif width && height
207
+ width = normalize_positive_number(width, :width)
208
+ height = normalize_positive_number(height, :height)
209
+ else
210
+ raise ArgumentError, "scatter requires width and height together, or radius"
211
+ end
212
+
213
+ points = []
214
+ point_count.times do
215
+ if radius
216
+ points << sample_scatter_radius(random, scatter_radius, origin_x, origin_y)
217
+ else
218
+ points << [
219
+ origin_x + (random.rand - 0.5) * width,
220
+ origin_y + (random.rand - 0.5) * height
221
+ ]
222
+ end
223
+ end
224
+ points
225
+ end
226
+
227
+ private
228
+
229
+ def sample_scatter_radius(random, radius, origin_x, origin_y)
230
+ loop do
231
+ x = random.rand * 2.0 - 1.0
232
+ y = random.rand * 2.0 - 1.0
233
+ if x * x + y * y <= 1.0
234
+ return [
235
+ normalize_layout_coordinate(origin_x + x * radius),
236
+ normalize_layout_coordinate(origin_y + y * radius)
237
+ ]
238
+ end
239
+ end
240
+ end
241
+
242
+ def normalize_xy_pair(value, name)
243
+ values = Array(value)
244
+ raise ArgumentError, "#{name} must include x and y coordinates" if values.length != 2
245
+ values.map { |part| normalize_numeric(part, name) }
246
+ end
247
+
248
+ def normalize_positive_integer(value, name)
249
+ Integer(value).tap do |integer|
250
+ raise ArgumentError, "#{name} must be positive" unless integer > 0
251
+ end
252
+ rescue ArgumentError, TypeError
253
+ raise ArgumentError, "#{name} must be a positive integer"
254
+ end
255
+
256
+ def normalize_positive_number(value, name)
257
+ number = Float(value)
258
+ raise ArgumentError, "#{name} must be positive" unless number.positive?
259
+ number
260
+ rescue ArgumentError, TypeError
261
+ raise ArgumentError, "#{name} must be a positive number"
262
+ end
263
+
264
+ def normalize_non_negative_number(value, name)
265
+ number = Float(value)
266
+ raise ArgumentError, "#{name} must be zero or more" if number.negative?
267
+ number
268
+ rescue ArgumentError, TypeError
269
+ raise ArgumentError, "#{name} must be a number"
270
+ end
271
+
272
+ def normalize_numeric(value, name)
273
+ Float(value)
274
+ rescue ArgumentError, TypeError
275
+ raise ArgumentError, "#{name} must be a number"
276
+ end
277
+
278
+ def degrees_to_radians(value)
279
+ Float(value) * (Math::PI / 180.0)
280
+ rescue ArgumentError, TypeError
281
+ raise ArgumentError, "angle must be numeric"
282
+ end
283
+
284
+ def normalize_layout_coordinate(value)
285
+ return 0.0 if value.abs < 1e-12
286
+ value
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "layer_builder"
4
+
5
+ module Vizcore
6
+ module DSL
7
+ # Collects reusable mapping definitions for layer-level reuse.
8
+ class MappingPresetBuilder
9
+ # @param name [Symbol, String] preset identifier
10
+ # @param strict [Boolean] strict mode behavior while building mapping preset
11
+ def initialize(name:, strict: false)
12
+ @name = name.to_sym
13
+ @strict = !!strict
14
+ @builder = LayerBuilder.new(name: "#{@name}_mapping_preset", strict: @strict)
15
+ end
16
+
17
+ # Evaluate mapping preset block.
18
+ #
19
+ # @yield DSL block
20
+ # @return [Vizcore::DSL::MappingPresetBuilder]
21
+ def evaluate(&block)
22
+ @builder.instance_eval(&block) if block
23
+ self
24
+ end
25
+
26
+ # @return [Hash] serialized mapping preset payload
27
+ def to_h
28
+ {
29
+ name: @name,
30
+ mappings: deep_dup(@builder.to_h[:mappings] || [])
31
+ }
32
+ end
33
+
34
+ private
35
+
36
+ def deep_dup(value)
37
+ Vizcore::DeepCopy.copy(value)
38
+ end
39
+ end
40
+ end
41
+ end