vizcore 1.0.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.
- checksums.yaml +4 -4
- data/README.md +66 -648
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/index.html +2 -1
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/frontend/index.html +26 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/main.js +268 -0
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/renderer/engine.js +10 -1
- data/frontend/src/renderer/layer-manager.js +18 -4
- data/frontend/src/shape-editor-controls.js +157 -0
- data/frontend/src/visuals/geometry.js +425 -27
- data/frontend/src/visuals/shape-renderer.js +475 -0
- data/frontend/src/visuals/svg-arc.js +104 -0
- data/lib/vizcore/cli/dsl_reference.rb +1 -1
- data/lib/vizcore/cli/scene_validator.rb +92 -0
- data/lib/vizcore/dsl/layer_builder.rb +795 -7
- data/lib/vizcore/dsl/mapping_resolver.rb +158 -4
- data/lib/vizcore/layer_catalog.rb +4 -2
- data/lib/vizcore/renderer/scene_frame_source.rb +14 -1
- data/lib/vizcore/renderer/snapshot_renderer.rb +507 -15
- data/lib/vizcore/server/frame_broadcaster.rb +53 -4
- data/lib/vizcore/server/runner.rb +21 -0
- data/lib/vizcore/shape.rb +719 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +1 -0
- data/sig/vizcore.rbs +100 -1
- metadata +12 -1
|
@@ -8,6 +8,9 @@ module Vizcore
|
|
|
8
8
|
class SnapshotRenderer
|
|
9
9
|
DEFAULT_WIDTH = 1280
|
|
10
10
|
DEFAULT_HEIGHT = 720
|
|
11
|
+
PATH_DEFAULT_MAX_SEGMENTS = 4096
|
|
12
|
+
PATH_HARD_MAX_SEGMENTS = 65_536
|
|
13
|
+
PATH_MAX_RECURSION = 12
|
|
11
14
|
PALETTE = [
|
|
12
15
|
[56, 189, 248],
|
|
13
16
|
[225, 29, 72],
|
|
@@ -156,6 +159,7 @@ module Vizcore
|
|
|
156
159
|
def render_shape_layer(canvas, layer, audio, color)
|
|
157
160
|
params = Hash(layer[:params] || layer["params"] || {})
|
|
158
161
|
shapes = Array(params[:shapes] || params["shapes"])
|
|
162
|
+
context = shape_coordinate_context(params)
|
|
159
163
|
pulse = clamp(audio[:beat_pulse])
|
|
160
164
|
alpha = 0.58 + pulse * 0.24
|
|
161
165
|
|
|
@@ -163,34 +167,522 @@ module Vizcore
|
|
|
163
167
|
shape_hash = Hash(shape)
|
|
164
168
|
case (shape_hash[:kind] || shape_hash["kind"]).to_s
|
|
165
169
|
when "circle"
|
|
166
|
-
render_circle_shape(canvas, shape_hash, color, alpha)
|
|
170
|
+
render_circle_shape(canvas, shape_hash, color, alpha, context)
|
|
167
171
|
when "line"
|
|
168
|
-
render_line_shape(canvas, shape_hash, color, alpha)
|
|
172
|
+
render_line_shape(canvas, shape_hash, color, alpha, context)
|
|
173
|
+
when "rect"
|
|
174
|
+
render_rect_shape(canvas, shape_hash, color, alpha, context)
|
|
175
|
+
when "polygon", "polyline"
|
|
176
|
+
render_polygon_shape(canvas, shape_hash, color, alpha, context)
|
|
177
|
+
when "path"
|
|
178
|
+
render_path_shape(canvas, shape_hash, color, alpha, context)
|
|
179
|
+
when "star"
|
|
180
|
+
render_star_shape(canvas, shape_hash, color, alpha, context)
|
|
169
181
|
end
|
|
170
182
|
end
|
|
171
183
|
rescue ArgumentError, TypeError
|
|
172
184
|
nil
|
|
173
185
|
end
|
|
174
186
|
|
|
175
|
-
def render_circle_shape(canvas, shape, color, alpha)
|
|
187
|
+
def render_circle_shape(canvas, shape, color, alpha, context)
|
|
176
188
|
count = [[Integer(shape[:count] || shape["count"] || 1), 1].max, 32].min
|
|
177
|
-
radius =
|
|
178
|
-
|
|
179
|
-
y = Float(shape[:y] || shape["y"] || height * 0.5)
|
|
180
|
-
x = width * 0.5 if x.abs <= 1.5
|
|
181
|
-
y = height * 0.5 if y.abs <= 1.5
|
|
189
|
+
radius = shape_length(shape[:radius] || shape["radius"] || 100, context, :radius)
|
|
190
|
+
center = shape_point(shape[:x] || shape["x"] || 0, shape[:y] || shape["y"] || 0, context)
|
|
182
191
|
|
|
183
192
|
count.times do |index|
|
|
184
|
-
|
|
193
|
+
ring_radius = radius * ((index + 1).to_f / count)
|
|
194
|
+
render_polyline_shape(canvas, circle_points(center, ring_radius), shape, color, alpha, context, closed: true)
|
|
185
195
|
end
|
|
186
196
|
end
|
|
187
197
|
|
|
188
|
-
def render_line_shape(canvas, shape, color, alpha)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
def render_line_shape(canvas, shape, color, alpha, context)
|
|
199
|
+
defaults = context[:units] == :legacy || context[:units] == :ndc ? [-0.8, 0, 0.8, 0] : [-100, 0, 100, 0]
|
|
200
|
+
from = shape_point(shape[:x1] || shape["x1"] || defaults[0], shape[:y1] || shape["y1"] || defaults[1], context)
|
|
201
|
+
to = shape_point(shape[:x2] || shape["x2"] || defaults[2], shape[:y2] || shape["y2"] || defaults[3], context)
|
|
202
|
+
draw_shape_segment(canvas, from, to, shape, color, alpha, context)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def render_rect_shape(canvas, shape, color, alpha, context)
|
|
206
|
+
center = shape_point(shape[:x] || shape["x"] || 0, shape[:y] || shape["y"] || 0, context)
|
|
207
|
+
half_width = shape_length(shape[:width] || shape["width"] || 100, context, :x) / 2.0
|
|
208
|
+
half_height = shape_length(shape[:height] || shape["height"] || 100, context, :y) / 2.0
|
|
209
|
+
points = [
|
|
210
|
+
[center[0] - half_width, center[1] - half_height],
|
|
211
|
+
[center[0] + half_width, center[1] - half_height],
|
|
212
|
+
[center[0] + half_width, center[1] + half_height],
|
|
213
|
+
[center[0] - half_width, center[1] + half_height]
|
|
214
|
+
]
|
|
215
|
+
render_polyline_shape(canvas, points, shape, color, alpha, context, closed: true)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def render_polygon_shape(canvas, shape, color, alpha, context)
|
|
219
|
+
points = Array(shape[:points] || shape["points"]).filter_map do |point|
|
|
220
|
+
values = Array(point)
|
|
221
|
+
next if values.length < 2
|
|
222
|
+
|
|
223
|
+
shape_point(values[0], values[1], context)
|
|
224
|
+
end
|
|
225
|
+
closed = (shape[:kind] || shape["kind"]).to_s == "polygon" ? shape.fetch(:closed, shape.fetch("closed", true)) : false
|
|
226
|
+
render_polyline_shape(canvas, points, shape, color, alpha, context, closed: closed)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def render_star_shape(canvas, shape, color, alpha, context)
|
|
230
|
+
tips = [[Integer(shape[:points] || shape["points"] || 5), 3].max, 128].min
|
|
231
|
+
center = shape_point(shape[:x] || shape["x"] || 0, shape[:y] || shape["y"] || 0, context)
|
|
232
|
+
radius = shape_length(shape[:radius] || shape["radius"] || 100, context, :radius)
|
|
233
|
+
inner_radius = shape_length(shape[:inner_radius] || shape["inner_radius"] || Float(shape[:radius] || shape["radius"] || 100) * 0.5, context, :radius)
|
|
234
|
+
rotation = Float(shape[:rotation] || shape["rotation"] || -90) * Math::PI / 180.0
|
|
235
|
+
points = (tips * 2).times.map do |index|
|
|
236
|
+
angle = rotation + (index.to_f / (tips * 2)) * Math::PI * 2
|
|
237
|
+
point_radius = index.even? ? radius : inner_radius
|
|
238
|
+
[center[0] + Math.cos(angle) * point_radius, center[1] - Math.sin(angle) * point_radius]
|
|
239
|
+
end
|
|
240
|
+
render_polyline_shape(canvas, points, shape, color, alpha, context, closed: true)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def render_path_shape(canvas, shape, color, alpha, context)
|
|
244
|
+
detail = [[Integer(shape[:detail] || shape["detail"] || 32), 4].max, 128].min
|
|
245
|
+
tolerance = path_tolerance(shape)
|
|
246
|
+
segment_budget = { remaining: path_segment_limit(shape) }
|
|
247
|
+
current = nil
|
|
248
|
+
subpath_start = nil
|
|
249
|
+
Array(shape[:commands] || shape["commands"]).each do |entry|
|
|
250
|
+
command, *values = Array(entry)
|
|
251
|
+
values = values.map { |value| Float(value) }
|
|
252
|
+
case command.to_s.upcase
|
|
253
|
+
when "M"
|
|
254
|
+
current = values.first(2)
|
|
255
|
+
subpath_start = current
|
|
256
|
+
when "L"
|
|
257
|
+
next unless current && values.length >= 2
|
|
258
|
+
|
|
259
|
+
current = draw_raw_path_segment(canvas, current, values.first(2), shape, color, alpha, context, segment_budget)
|
|
260
|
+
when "H"
|
|
261
|
+
next unless current && values.length >= 1
|
|
262
|
+
|
|
263
|
+
current = draw_raw_path_segment(canvas, current, [values[0], current[1]], shape, color, alpha, context, segment_budget)
|
|
264
|
+
when "V"
|
|
265
|
+
next unless current && values.length >= 1
|
|
266
|
+
|
|
267
|
+
current = draw_raw_path_segment(canvas, current, [current[0], values[0]], shape, color, alpha, context, segment_budget)
|
|
268
|
+
when "Q"
|
|
269
|
+
next unless current && values.length >= 4
|
|
270
|
+
|
|
271
|
+
current = draw_quadratic_path(canvas, current, values, detail, tolerance, shape, color, alpha, context, segment_budget)
|
|
272
|
+
when "C"
|
|
273
|
+
next unless current && values.length >= 6
|
|
274
|
+
|
|
275
|
+
current = draw_cubic_path(canvas, current, values, detail, tolerance, shape, color, alpha, context, segment_budget)
|
|
276
|
+
when "A"
|
|
277
|
+
next unless current && values.length >= 7
|
|
278
|
+
|
|
279
|
+
current = draw_arc_path(canvas, current, values, detail, shape, color, alpha, context, segment_budget)
|
|
280
|
+
when "Z"
|
|
281
|
+
if current && subpath_start
|
|
282
|
+
current = draw_raw_path_segment(canvas, current, subpath_start, shape, color, alpha, context, segment_budget)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def render_polyline_shape(canvas, points, shape, color, alpha, context, closed:)
|
|
289
|
+
return if points.length < 2
|
|
290
|
+
|
|
291
|
+
points.each_cons(2) { |from, to| draw_shape_segment(canvas, from, to, shape, color, alpha, context) }
|
|
292
|
+
draw_shape_segment(canvas, points.last, points.first, shape, color, alpha, context) if closed && points.length > 2
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def draw_raw_path_segment(canvas, from, to, shape, color, alpha, context, segment_budget = nil)
|
|
296
|
+
return to if segment_budget && segment_budget[:remaining] <= 0
|
|
297
|
+
|
|
298
|
+
draw_shape_segment(canvas, shape_point(from[0], from[1], context), shape_point(to[0], to[1], context), shape, color, alpha, context)
|
|
299
|
+
segment_budget[:remaining] -= 1 if segment_budget
|
|
300
|
+
to
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def draw_quadratic_path(canvas, current, values, detail, tolerance, shape, color, alpha, context, segment_budget = nil)
|
|
304
|
+
previous = current
|
|
305
|
+
control = values.first(2)
|
|
306
|
+
endpoint = values.last(2)
|
|
307
|
+
if tolerance
|
|
308
|
+
draw_adaptive_quadratic_path(canvas, current, control, endpoint, tolerance, shape, color, alpha, context, segment_budget)
|
|
309
|
+
return endpoint
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
1.upto(detail) do |step|
|
|
313
|
+
break if segment_budget && segment_budget[:remaining] <= 0
|
|
314
|
+
|
|
315
|
+
t = step.to_f / detail
|
|
316
|
+
point = [
|
|
317
|
+
quadratic_point(current[0], control[0], endpoint[0], t),
|
|
318
|
+
quadratic_point(current[1], control[1], endpoint[1], t)
|
|
319
|
+
]
|
|
320
|
+
draw_raw_path_segment(canvas, previous, point, shape, color, alpha, context, segment_budget)
|
|
321
|
+
previous = point
|
|
322
|
+
end
|
|
323
|
+
endpoint
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def draw_cubic_path(canvas, current, values, detail, tolerance, shape, color, alpha, context, segment_budget = nil)
|
|
327
|
+
previous = current
|
|
328
|
+
c1 = values[0, 2]
|
|
329
|
+
c2 = values[2, 2]
|
|
330
|
+
endpoint = values[4, 2]
|
|
331
|
+
if tolerance
|
|
332
|
+
draw_adaptive_cubic_path(canvas, current, c1, c2, endpoint, tolerance, shape, color, alpha, context, segment_budget)
|
|
333
|
+
return endpoint
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
1.upto(detail) do |step|
|
|
337
|
+
break if segment_budget && segment_budget[:remaining] <= 0
|
|
338
|
+
|
|
339
|
+
t = step.to_f / detail
|
|
340
|
+
point = [
|
|
341
|
+
cubic_point(current[0], c1[0], c2[0], endpoint[0], t),
|
|
342
|
+
cubic_point(current[1], c1[1], c2[1], endpoint[1], t)
|
|
343
|
+
]
|
|
344
|
+
draw_raw_path_segment(canvas, previous, point, shape, color, alpha, context, segment_budget)
|
|
345
|
+
previous = point
|
|
346
|
+
end
|
|
347
|
+
endpoint
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def draw_arc_path(canvas, current, values, detail, shape, color, alpha, context, segment_budget = nil)
|
|
351
|
+
endpoint = values[5, 2]
|
|
352
|
+
arc = svg_arc_description(
|
|
353
|
+
from: current,
|
|
354
|
+
to: endpoint,
|
|
355
|
+
rx: values[0],
|
|
356
|
+
ry: values[1],
|
|
357
|
+
x_axis_rotation: values[2],
|
|
358
|
+
large_arc: arc_flag(values[3]),
|
|
359
|
+
sweep: arc_flag(values[4])
|
|
360
|
+
)
|
|
361
|
+
unless arc
|
|
362
|
+
return draw_raw_path_segment(canvas, current, endpoint, shape, color, alpha, context, segment_budget)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
previous = current
|
|
366
|
+
segments = svg_arc_segment_count(arc, detail)
|
|
367
|
+
1.upto(segments) do |step|
|
|
368
|
+
break if segment_budget && segment_budget[:remaining] <= 0
|
|
369
|
+
|
|
370
|
+
point = svg_arc_point(arc, step.to_f / segments)
|
|
371
|
+
draw_raw_path_segment(canvas, previous, point, shape, color, alpha, context, segment_budget)
|
|
372
|
+
previous = point
|
|
373
|
+
end
|
|
374
|
+
endpoint
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def path_segment_limit(shape)
|
|
378
|
+
raw_value = shape[:max_segments] || shape["max_segments"] || PATH_DEFAULT_MAX_SEGMENTS
|
|
379
|
+
[[Integer(raw_value), 1].max, PATH_HARD_MAX_SEGMENTS].min
|
|
380
|
+
rescue ArgumentError, TypeError
|
|
381
|
+
PATH_DEFAULT_MAX_SEGMENTS
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def path_tolerance(shape)
|
|
385
|
+
return unless shape.key?(:tolerance) || shape.key?("tolerance")
|
|
386
|
+
|
|
387
|
+
value = Float(shape[:tolerance] || shape["tolerance"])
|
|
388
|
+
value if value.finite? && value >= 0
|
|
389
|
+
rescue ArgumentError, TypeError
|
|
390
|
+
nil
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def draw_adaptive_quadratic_path(canvas, from, control, to, tolerance, shape, color, alpha, context, segment_budget, depth = 0)
|
|
394
|
+
return if segment_budget && segment_budget[:remaining] <= 0
|
|
395
|
+
|
|
396
|
+
if depth >= PATH_MAX_RECURSION || point_line_distance(control, from, to) <= tolerance
|
|
397
|
+
draw_raw_path_segment(canvas, from, to, shape, color, alpha, context, segment_budget)
|
|
398
|
+
return
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
left_control = midpoint(from, control)
|
|
402
|
+
right_control = midpoint(control, to)
|
|
403
|
+
center = midpoint(left_control, right_control)
|
|
404
|
+
draw_adaptive_quadratic_path(canvas, from, left_control, center, tolerance, shape, color, alpha, context, segment_budget, depth + 1)
|
|
405
|
+
draw_adaptive_quadratic_path(canvas, center, right_control, to, tolerance, shape, color, alpha, context, segment_budget, depth + 1)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def draw_adaptive_cubic_path(canvas, from, c1, c2, to, tolerance, shape, color, alpha, context, segment_budget, depth = 0)
|
|
409
|
+
return if segment_budget && segment_budget[:remaining] <= 0
|
|
410
|
+
|
|
411
|
+
flatness = [point_line_distance(c1, from, to), point_line_distance(c2, from, to)].max
|
|
412
|
+
if depth >= PATH_MAX_RECURSION || flatness <= tolerance
|
|
413
|
+
draw_raw_path_segment(canvas, from, to, shape, color, alpha, context, segment_budget)
|
|
414
|
+
return
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
p01 = midpoint(from, c1)
|
|
418
|
+
p12 = midpoint(c1, c2)
|
|
419
|
+
p23 = midpoint(c2, to)
|
|
420
|
+
p012 = midpoint(p01, p12)
|
|
421
|
+
p123 = midpoint(p12, p23)
|
|
422
|
+
center = midpoint(p012, p123)
|
|
423
|
+
draw_adaptive_cubic_path(canvas, from, p01, p012, center, tolerance, shape, color, alpha, context, segment_budget, depth + 1)
|
|
424
|
+
draw_adaptive_cubic_path(canvas, center, p123, p23, to, tolerance, shape, color, alpha, context, segment_budget, depth + 1)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def midpoint(from, to)
|
|
428
|
+
[(from[0] + to[0]) * 0.5, (from[1] + to[1]) * 0.5]
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def point_line_distance(point, from, to)
|
|
432
|
+
dx = to[0] - from[0]
|
|
433
|
+
dy = to[1] - from[1]
|
|
434
|
+
length = Math.sqrt((dx * dx) + (dy * dy))
|
|
435
|
+
return Math.sqrt(((point[0] - from[0])**2) + ((point[1] - from[1])**2)) if length <= 0
|
|
436
|
+
|
|
437
|
+
((dy * point[0]) - (dx * point[1]) + (to[0] * from[1]) - (to[1] * from[0])).abs / length
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def draw_shape_segment(canvas, from, to, shape, color, alpha, context)
|
|
441
|
+
from = apply_shape_transform(from, shape, context)
|
|
442
|
+
to = apply_shape_transform(to, shape, context)
|
|
443
|
+
canvas.draw_line(from[0], from[1], to[0], to[1], color, alpha: alpha * shape_opacity(shape))
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def shape_coordinate_context(params)
|
|
447
|
+
units = (params[:units] || params["units"]).to_s.strip.downcase
|
|
448
|
+
version = Integer(params[:shape_schema_version] || params["shape_schema_version"] || 1)
|
|
449
|
+
{ units: (units.empty? ? (version >= 2 ? :logical : :legacy) : units.to_sym) }
|
|
450
|
+
rescue ArgumentError, TypeError
|
|
451
|
+
{ units: :legacy }
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def shape_point(x, y, context)
|
|
455
|
+
[shape_coordinate(x, context, :x), shape_coordinate(y, context, :y)]
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def shape_coordinate(value, context, axis)
|
|
459
|
+
numeric = Float(value || 0)
|
|
460
|
+
case context[:units]
|
|
461
|
+
when :ndc
|
|
462
|
+
axis == :x ? width * 0.5 + numeric * width * 0.5 : height * 0.5 - numeric * height * 0.5
|
|
463
|
+
when :logical, :center, :center_origin, :px
|
|
464
|
+
axis == :x ? width * 0.5 + numeric : height * 0.5 - numeric
|
|
465
|
+
when :screen, :canvas, :viewport
|
|
466
|
+
numeric
|
|
467
|
+
else
|
|
468
|
+
legacy_shape_coordinate(numeric, axis)
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def legacy_shape_coordinate(value, axis)
|
|
473
|
+
return axis == :x ? width * 0.5 + value * width * 0.5 : height * 0.5 - value * height * 0.5 if value.abs <= 1.5
|
|
474
|
+
|
|
475
|
+
value
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def shape_length(value, context, _axis)
|
|
479
|
+
numeric = Float(value || 0).abs
|
|
480
|
+
return numeric * [width, height].min * 0.5 if context[:units] == :ndc || numeric <= 2
|
|
481
|
+
|
|
482
|
+
numeric
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def circle_points(center, radius)
|
|
486
|
+
segments = 96
|
|
487
|
+
segments.times.map do |index|
|
|
488
|
+
angle = (index.to_f / segments) * Math::PI * 2
|
|
489
|
+
[center[0] + Math.cos(angle) * radius, center[1] + Math.sin(angle) * radius]
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def apply_shape_transform(point, shape, context)
|
|
494
|
+
transform = shape_transform(shape, context)
|
|
495
|
+
shifted_x = (point[0] - transform[:origin][0]) * transform[:scale][:x]
|
|
496
|
+
shifted_y = (point[1] - transform[:origin][1]) * transform[:scale][:y]
|
|
497
|
+
radians = -transform[:rotate] * Math::PI / 180.0
|
|
498
|
+
cos = Math.cos(radians)
|
|
499
|
+
sin = Math.sin(radians)
|
|
500
|
+
rotated_x = shifted_x * cos - shifted_y * sin
|
|
501
|
+
rotated_y = shifted_x * sin + shifted_y * cos
|
|
502
|
+
|
|
503
|
+
[
|
|
504
|
+
rotated_x + transform[:origin][0] + transform[:translate][:x],
|
|
505
|
+
rotated_y + transform[:origin][1] + transform[:translate][:y]
|
|
506
|
+
]
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def shape_transform(shape, context)
|
|
510
|
+
transform = Hash(shape[:transform] || shape["transform"] || {})
|
|
511
|
+
{
|
|
512
|
+
translate: shape_vector_pair(shape_hash_value(transform, :translate) || shape_hash_value(shape, :translate), context),
|
|
513
|
+
origin: shape_origin_pair(shape_hash_value(transform, :origin) || shape_hash_value(shape, :origin), context),
|
|
514
|
+
rotate: Float(shape_hash_value(transform, :rotate) || shape_hash_value(shape, :rotate) || shape_hash_value(shape, :rotation) || 0),
|
|
515
|
+
scale: shape_scale(shape_hash_value(transform, :scale) || shape_hash_value(shape, :scale))
|
|
516
|
+
}
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def shape_vector_pair(value, context)
|
|
520
|
+
if value.is_a?(Array)
|
|
521
|
+
return { x: shape_vector(value[0], context, :x), y: shape_vector(value[1], context, :y) }
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
values = value.is_a?(Hash) ? value : {}
|
|
525
|
+
{ x: shape_vector(shape_hash_value(values, :x) || 0, context, :x), y: shape_vector(shape_hash_value(values, :y) || 0, context, :y) }
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def shape_origin_pair(value, context)
|
|
529
|
+
if value.is_a?(Array)
|
|
530
|
+
return shape_point(value[0], value[1], context)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
values = value.is_a?(Hash) ? value : {}
|
|
534
|
+
shape_point(shape_hash_value(values, :x) || 0, shape_hash_value(values, :y) || 0, context)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def shape_vector(value, context, axis)
|
|
538
|
+
numeric = Float(value || 0)
|
|
539
|
+
case context[:units]
|
|
540
|
+
when :ndc
|
|
541
|
+
axis == :x ? numeric * width * 0.5 : -numeric * height * 0.5
|
|
542
|
+
else
|
|
543
|
+
axis == :x ? numeric : -numeric
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def shape_scale(value)
|
|
548
|
+
if value.is_a?(Hash)
|
|
549
|
+
return {
|
|
550
|
+
x: Float(shape_hash_value(value, :x) || 1).clamp(-8.0, 8.0),
|
|
551
|
+
y: Float(shape_hash_value(value, :y) || 1).clamp(-8.0, 8.0)
|
|
552
|
+
}
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
scale = Float(value || 1).clamp(-8.0, 8.0)
|
|
556
|
+
{ x: scale, y: scale }
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def shape_opacity(shape)
|
|
560
|
+
Float(shape[:opacity] || shape["opacity"] || 1).clamp(0.0, 1.0)
|
|
561
|
+
rescue ArgumentError, TypeError
|
|
562
|
+
1.0
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def shape_hash_value(hash, key)
|
|
566
|
+
hash[key] || hash[key.to_s]
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def quadratic_point(from, control, to, t)
|
|
570
|
+
inv = 1.0 - t
|
|
571
|
+
inv * inv * from + 2 * inv * t * control + t * t * to
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def cubic_point(from, c1, c2, to, t)
|
|
575
|
+
inv = 1.0 - t
|
|
576
|
+
inv * inv * inv * from + 3 * inv * inv * t * c1 + 3 * inv * t * t * c2 + t * t * t * to
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def svg_arc_description(from:, to:, rx:, ry:, x_axis_rotation:, large_arc:, sweep:)
|
|
580
|
+
return if same_point?(from, to)
|
|
581
|
+
|
|
582
|
+
radius_x = Float(rx || 0).abs
|
|
583
|
+
radius_y = Float(ry || 0).abs
|
|
584
|
+
return if radius_x <= 0 || radius_y <= 0
|
|
585
|
+
|
|
586
|
+
rotation = Float(x_axis_rotation || 0) * Math::PI / 180.0
|
|
587
|
+
cos = Math.cos(rotation)
|
|
588
|
+
sin = Math.sin(rotation)
|
|
589
|
+
dx = (from[0] - to[0]) / 2.0
|
|
590
|
+
dy = (from[1] - to[1]) / 2.0
|
|
591
|
+
x1p = cos * dx + sin * dy
|
|
592
|
+
y1p = -sin * dx + cos * dy
|
|
593
|
+
|
|
594
|
+
scale = (x1p * x1p / (radius_x * radius_x)) + (y1p * y1p / (radius_y * radius_y))
|
|
595
|
+
if scale > 1
|
|
596
|
+
multiplier = Math.sqrt(scale)
|
|
597
|
+
radius_x *= multiplier
|
|
598
|
+
radius_y *= multiplier
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
center = svg_arc_center(
|
|
602
|
+
from: from,
|
|
603
|
+
to: to,
|
|
604
|
+
radius_x: radius_x,
|
|
605
|
+
radius_y: radius_y,
|
|
606
|
+
x1p: x1p,
|
|
607
|
+
y1p: y1p,
|
|
608
|
+
rotation_cos: cos,
|
|
609
|
+
rotation_sin: sin,
|
|
610
|
+
large_arc: large_arc,
|
|
611
|
+
sweep: sweep
|
|
612
|
+
)
|
|
613
|
+
return unless center
|
|
614
|
+
|
|
615
|
+
start_vector = [(x1p - center[:cxp]) / radius_x, (y1p - center[:cyp]) / radius_y]
|
|
616
|
+
end_vector = [(-x1p - center[:cxp]) / radius_x, (-y1p - center[:cyp]) / radius_y]
|
|
617
|
+
start_angle = vector_angle([1.0, 0.0], start_vector)
|
|
618
|
+
delta_angle = vector_angle(start_vector, end_vector)
|
|
619
|
+
delta_angle -= Math::PI * 2 if !sweep && delta_angle.positive?
|
|
620
|
+
delta_angle += Math::PI * 2 if sweep && delta_angle.negative?
|
|
621
|
+
|
|
622
|
+
{
|
|
623
|
+
cx: center[:cx],
|
|
624
|
+
cy: center[:cy],
|
|
625
|
+
rx: radius_x,
|
|
626
|
+
ry: radius_y,
|
|
627
|
+
rotation: rotation,
|
|
628
|
+
start_angle: start_angle,
|
|
629
|
+
delta_angle: delta_angle
|
|
630
|
+
}
|
|
631
|
+
rescue ArgumentError, TypeError
|
|
632
|
+
nil
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def svg_arc_center(from:, to:, radius_x:, radius_y:, x1p:, y1p:, rotation_cos:, rotation_sin:, large_arc:, sweep:)
|
|
636
|
+
rx2 = radius_x * radius_x
|
|
637
|
+
ry2 = radius_y * radius_y
|
|
638
|
+
x1p2 = x1p * x1p
|
|
639
|
+
y1p2 = y1p * y1p
|
|
640
|
+
denominator = rx2 * y1p2 + ry2 * x1p2
|
|
641
|
+
return if denominator.zero?
|
|
642
|
+
|
|
643
|
+
numerator = [rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2, 0.0].max
|
|
644
|
+
sign = large_arc == sweep ? -1.0 : 1.0
|
|
645
|
+
coefficient = sign * Math.sqrt(numerator / denominator)
|
|
646
|
+
cxp = coefficient * ((radius_x * y1p) / radius_y)
|
|
647
|
+
cyp = coefficient * (-(radius_y * x1p) / radius_x)
|
|
648
|
+
{
|
|
649
|
+
cxp: cxp,
|
|
650
|
+
cyp: cyp,
|
|
651
|
+
cx: rotation_cos * cxp - rotation_sin * cyp + (from[0] + to[0]) / 2.0,
|
|
652
|
+
cy: rotation_sin * cxp + rotation_cos * cyp + (from[1] + to[1]) / 2.0
|
|
653
|
+
}
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def svg_arc_point(arc, progress)
|
|
657
|
+
angle = arc[:start_angle] + arc[:delta_angle] * progress
|
|
658
|
+
cos_rotation = Math.cos(arc[:rotation])
|
|
659
|
+
sin_rotation = Math.sin(arc[:rotation])
|
|
660
|
+
x = Math.cos(angle) * arc[:rx]
|
|
661
|
+
y = Math.sin(angle) * arc[:ry]
|
|
662
|
+
[
|
|
663
|
+
arc[:cx] + cos_rotation * x - sin_rotation * y,
|
|
664
|
+
arc[:cy] + sin_rotation * x + cos_rotation * y
|
|
665
|
+
]
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def svg_arc_segment_count(arc, detail)
|
|
669
|
+
[((arc[:delta_angle].abs / (Math::PI * 2)) * detail).ceil, 1].max
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def vector_angle(from, to)
|
|
673
|
+
cross = from[0] * to[1] - from[1] * to[0]
|
|
674
|
+
dot = from[0] * to[0] + from[1] * to[1]
|
|
675
|
+
Math.atan2(cross, dot)
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def same_point?(from, to)
|
|
679
|
+
(from[0] - to[0]).abs < 1e-9 && (from[1] - to[1]).abs < 1e-9
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def arc_flag(value)
|
|
683
|
+
!Float(value || 0).zero?
|
|
684
|
+
rescue ArgumentError, TypeError
|
|
685
|
+
false
|
|
194
686
|
end
|
|
195
687
|
|
|
196
688
|
def render_mesh_layer(canvas, layer, audio, color, index)
|
|
@@ -67,6 +67,8 @@ module Vizcore
|
|
|
67
67
|
@error_reporter = error_reporter || ->(_message) {}
|
|
68
68
|
@last_error = nil
|
|
69
69
|
@frame_count = 0
|
|
70
|
+
@custom_shape_param_overrides = {}
|
|
71
|
+
@custom_shape_param_mutex = Mutex.new
|
|
70
72
|
@transport_playing = initial_transport_playing_state
|
|
71
73
|
reset_transition_trigger_counters!
|
|
72
74
|
@tap_tempo = Vizcore::Analysis::TapTempo.new
|
|
@@ -210,18 +212,35 @@ module Vizcore
|
|
|
210
212
|
true
|
|
211
213
|
end
|
|
212
214
|
|
|
215
|
+
def set_custom_shape_param(layer_name:, custom_shape_index:, param:, value:)
|
|
216
|
+
layer_key = layer_name.to_s
|
|
217
|
+
param_key = param.to_s.strip
|
|
218
|
+
index = Integer(custom_shape_index)
|
|
219
|
+
numeric = finite_float(value)
|
|
220
|
+
return custom_shape_param_overrides_snapshot if layer_key.empty? || param_key.empty? || index.negative? || numeric.nil?
|
|
221
|
+
|
|
222
|
+
@custom_shape_param_mutex.synchronize do
|
|
223
|
+
@custom_shape_param_overrides[layer_key] ||= {}
|
|
224
|
+
@custom_shape_param_overrides[layer_key][index] ||= {}
|
|
225
|
+
@custom_shape_param_overrides[layer_key][index][param_key] = numeric
|
|
226
|
+
deep_dup(@custom_shape_param_overrides)
|
|
227
|
+
end
|
|
228
|
+
rescue ArgumentError, TypeError
|
|
229
|
+
custom_shape_param_overrides_snapshot
|
|
230
|
+
end
|
|
231
|
+
|
|
213
232
|
# Build one frame payload for transport to frontend.
|
|
214
233
|
#
|
|
215
234
|
# @param _elapsed_seconds [Float]
|
|
216
235
|
# @param samples [Array<Float>, nil]
|
|
217
236
|
# @raise [Vizcore::FrameBuildError] when frame construction fails
|
|
218
237
|
# @return [Hash]
|
|
219
|
-
def build_frame(
|
|
238
|
+
def build_frame(elapsed_seconds, samples = nil)
|
|
220
239
|
started_at_ms = monotonic_ms
|
|
221
240
|
audio_samples, audio_capture_ms = capture_or_use_samples(samples)
|
|
222
241
|
analyzed, audio_analysis_ms = measure_ms { @analysis_pipeline.call(audio_samples) }
|
|
223
242
|
scene = current_scene
|
|
224
|
-
layers, scene_build_ms = measure_ms { build_scene_layers(scene[:layers], analyzed) }
|
|
243
|
+
layers, scene_build_ms = measure_ms { build_scene_layers(scene[:layers], analyzed, time: elapsed_seconds, frame: @frame_count) }
|
|
225
244
|
|
|
226
245
|
@scene_serializer.audio_frame(
|
|
227
246
|
timestamp: Time.now.to_f,
|
|
@@ -293,10 +312,40 @@ module Vizcore
|
|
|
293
312
|
value.positive? && (value & (value - 1)).zero?
|
|
294
313
|
end
|
|
295
314
|
|
|
296
|
-
def build_scene_layers(scene_layers, analyzed)
|
|
315
|
+
def build_scene_layers(scene_layers, analyzed, time: 0.0, frame: 0)
|
|
297
316
|
return default_scene_layers(analyzed) if scene_layers.empty?
|
|
298
317
|
|
|
299
|
-
@mapping_resolver.resolve_layers(
|
|
318
|
+
@mapping_resolver.resolve_layers(
|
|
319
|
+
scene_layers: scene_layers,
|
|
320
|
+
audio: analyzed,
|
|
321
|
+
time: time,
|
|
322
|
+
frame: frame,
|
|
323
|
+
custom_shape_overrides: custom_shape_param_overrides_snapshot
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def custom_shape_param_overrides_snapshot
|
|
328
|
+
@custom_shape_param_mutex.synchronize { deep_dup(@custom_shape_param_overrides) }
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def finite_float(value)
|
|
332
|
+
numeric = Float(value)
|
|
333
|
+
return nil unless numeric.finite?
|
|
334
|
+
|
|
335
|
+
numeric
|
|
336
|
+
rescue ArgumentError, TypeError
|
|
337
|
+
nil
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def deep_dup(value)
|
|
341
|
+
case value
|
|
342
|
+
when Hash
|
|
343
|
+
value.each_with_object({}) { |(key, entry), output| output[key] = deep_dup(entry) }
|
|
344
|
+
when Array
|
|
345
|
+
value.map { |entry| deep_dup(entry) }
|
|
346
|
+
else
|
|
347
|
+
value
|
|
348
|
+
end
|
|
300
349
|
end
|
|
301
350
|
|
|
302
351
|
def default_scene_layers(analyzed)
|
|
@@ -398,6 +398,8 @@ module Vizcore
|
|
|
398
398
|
switch_scene_from_client(target_name, broadcaster)
|
|
399
399
|
when "tap_tempo"
|
|
400
400
|
apply_tap_tempo(payload, broadcaster)
|
|
401
|
+
when "custom_shape_param"
|
|
402
|
+
apply_custom_shape_param(payload, broadcaster)
|
|
401
403
|
end
|
|
402
404
|
rescue StandardError => e
|
|
403
405
|
@output.puts(Vizcore::ErrorFormatting.summarize(e, context: "Client control message failed"))
|
|
@@ -616,6 +618,25 @@ module Vizcore
|
|
|
616
618
|
)
|
|
617
619
|
end
|
|
618
620
|
|
|
621
|
+
def apply_custom_shape_param(payload, broadcaster)
|
|
622
|
+
return unless broadcaster.respond_to?(:set_custom_shape_param)
|
|
623
|
+
|
|
624
|
+
values = Hash(payload)
|
|
625
|
+
overrides = broadcaster.set_custom_shape_param(
|
|
626
|
+
layer_name: values["layer"] || values[:layer] || values["layer_name"] || values[:layer_name],
|
|
627
|
+
custom_shape_index: values["custom_shape_index"] || values[:custom_shape_index] || values["index"] || values[:index],
|
|
628
|
+
param: values["param"] || values[:param],
|
|
629
|
+
value: values["value"] || values[:value]
|
|
630
|
+
)
|
|
631
|
+
WebSocketHandler.broadcast(
|
|
632
|
+
type: "config_update",
|
|
633
|
+
payload: {
|
|
634
|
+
custom_shape_params: overrides,
|
|
635
|
+
source: "ui"
|
|
636
|
+
}
|
|
637
|
+
)
|
|
638
|
+
end
|
|
639
|
+
|
|
619
640
|
def apply_osc_live_control(control, value)
|
|
620
641
|
@live_controls[control] = osc_truthy?(value)
|
|
621
642
|
WebSocketHandler.broadcast(
|