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.
- checksums.yaml +4 -4
- data/frontend/index.html +24 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +447 -57
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +391 -10
- data/frontend/src/renderer/layer-manager.js +472 -71
- data/frontend/src/runtime-control-preset.js +44 -0
- data/frontend/src/scene-patches.js +159 -0
- data/frontend/src/shader-error-overlay.js +1 -0
- data/frontend/src/visuals/image-renderer.js +19 -0
- data/frontend/src/visuals/particle-system.js +10 -0
- data/frontend/src/visuals/shape-renderer.js +13 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -0
- data/frontend/src/visuals/text-renderer.js +13 -0
- data/frontend/src/websocket-client.js +6 -0
- data/lib/vizcore/analysis/adaptive_normalizer.rb +20 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +18 -8
- data/lib/vizcore/analysis/feature_recorder.rb +117 -7
- data/lib/vizcore/analysis/feature_replay.rb +48 -9
- data/lib/vizcore/analysis/pipeline.rb +258 -9
- data/lib/vizcore/analysis/tap_tempo.rb +17 -2
- data/lib/vizcore/audio/calibration.rb +156 -0
- data/lib/vizcore/audio/file_input.rb +28 -0
- data/lib/vizcore/audio/input_manager.rb +36 -1
- data/lib/vizcore/audio/midi_input.rb +5 -0
- data/lib/vizcore/audio/ring_buffer.rb +22 -0
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/dsl_reference.rb +64 -8
- data/lib/vizcore/cli/plugin_checker.rb +93 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +2 -2
- data/lib/vizcore/cli/scene_inspector.rb +35 -1
- data/lib/vizcore/cli/scene_validator.rb +487 -39
- data/lib/vizcore/cli/shader_template.rb +7 -2
- data/lib/vizcore/cli/shader_uniform_docs.rb +11 -0
- data/lib/vizcore/cli.rb +268 -15
- data/lib/vizcore/config.rb +40 -3
- data/lib/vizcore/control_preset.rb +29 -0
- data/lib/vizcore/deep_copy.rb +21 -0
- data/lib/vizcore/dsl/color_helpers.rb +155 -0
- data/lib/vizcore/dsl/engine.rb +219 -23
- data/lib/vizcore/dsl/layer_builder.rb +278 -15
- data/lib/vizcore/dsl/layer_group_builder.rb +10 -12
- data/lib/vizcore/dsl/layout_helpers.rb +290 -0
- data/lib/vizcore/dsl/mapping_preset_builder.rb +41 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +404 -22
- data/lib/vizcore/dsl/mapping_transform_builder.rb +50 -0
- data/lib/vizcore/dsl/midi_map_executor.rb +219 -23
- data/lib/vizcore/dsl/reaction_builder.rb +1 -0
- data/lib/vizcore/dsl/scene_builder.rb +83 -13
- data/lib/vizcore/dsl/shader_source_resolver.rb +1 -10
- data/lib/vizcore/dsl/style_builder.rb +3 -0
- data/lib/vizcore/dsl/timeline_builder.rb +91 -8
- data/lib/vizcore/dsl/transition_controller.rb +157 -18
- data/lib/vizcore/dsl.rb +2 -0
- data/lib/vizcore/layer_catalog.rb +1 -0
- data/lib/vizcore/plugin_asset_policy.rb +55 -0
- data/lib/vizcore/project_manifest.rb +12 -2
- data/lib/vizcore/renderer/render_sequence.rb +104 -13
- data/lib/vizcore/renderer/scene_frame_source.rb +179 -14
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +134 -8
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +469 -23
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +676 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +39 -16
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +33 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +55 -4
- 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
|