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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../shape"
4
+
3
5
  module Vizcore
4
6
  module DSL
5
7
  # Resolves `map` definitions into concrete per-layer parameter values.
@@ -11,17 +13,19 @@ module Vizcore
11
13
  # @param scene_layers [Array<Hash>]
12
14
  # @param audio [Hash]
13
15
  # @return [Array<Hash>] normalized layer payloads with resolved params
14
- def resolve_layers(scene_layers:, audio:)
16
+ def resolve_layers(scene_layers:, audio:, time: 0.0, frame: 0, resolution: [1280, 720], globals: {}, custom_shape_overrides: {})
15
17
  normalize_scene_layers(scene_layers).map do |layer|
16
- resolve_layer(layer, audio)
18
+ resolve_layer(layer, audio, time: time, frame: frame, resolution: resolution, globals: globals, custom_shape_overrides: custom_shape_overrides)
17
19
  end
18
20
  end
19
21
 
20
22
  private
21
23
 
22
- def resolve_layer(layer, audio)
23
- params = (layer[:params] || {}).dup
24
+ def resolve_layer(layer, audio, time:, frame:, resolution:, globals:, custom_shape_overrides:)
25
+ params = deep_dup(layer[:params] || {})
26
+ apply_custom_shape_overrides!(params, layer_name: layer[:name], custom_shape_overrides: custom_shape_overrides)
24
27
  merge_resolved_mappings!(params, resolve_mappings(layer[:mappings], audio, layer_name: layer[:name]))
28
+ expand_dynamic_custom_shapes!(params, layer: layer, audio: audio, time: time, frame: frame, resolution: resolution, globals: globals)
25
29
 
26
30
  output = {
27
31
  name: layer.fetch(:name).to_s,
@@ -57,6 +61,95 @@ module Vizcore
57
61
  end
58
62
  end
59
63
 
64
+ def expand_dynamic_custom_shapes!(params, layer:, audio:, time:, frame:, resolution:, globals:)
65
+ descriptors = Array(params.delete(:custom_shapes) || params.delete("custom_shapes"))
66
+ return if descriptors.empty?
67
+
68
+ params[:shapes] = Array(params[:shapes])
69
+ controls = []
70
+ descriptors.each_with_index do |descriptor, index|
71
+ start_index = params[:shapes].length
72
+ expanded = expand_dynamic_custom_shape(descriptor, layer: layer, palette: params[:palette], audio: audio, time: time, frame: frame, resolution: resolution, globals: globals)
73
+ params[:shapes].concat(expanded)
74
+ controls << custom_shape_control_descriptor(descriptor, index: index, start_index: start_index, count: expanded.length)
75
+ end
76
+ params[:custom_shape_controls] = controls unless controls.empty?
77
+ end
78
+
79
+ def expand_dynamic_custom_shape(descriptor, layer:, palette:, audio:, time:, frame:, resolution:, globals:)
80
+ values = Hash(descriptor)
81
+ renderer = values.fetch(:renderer)
82
+ shape_name = values[:name] || renderer
83
+ primitives = Vizcore::Shape.expand_custom_shape(
84
+ renderer,
85
+ params: Hash(values[:params] || {}),
86
+ shape_id: values[:shape_id],
87
+ layer_name: layer[:name],
88
+ palette: Array(palette),
89
+ audio: audio,
90
+ time: time,
91
+ frame: frame,
92
+ resolution: resolution,
93
+ globals: globals,
94
+ shape_name: shape_name
95
+ )
96
+ primitives.each { |primitive| apply_custom_shape_attributes!(primitive, values) }
97
+ end
98
+
99
+ def custom_shape_control_descriptor(descriptor, index:, start_index:, count:)
100
+ values = Hash(descriptor)
101
+ {
102
+ index: index,
103
+ name: (values[:name] || values["name"] || "custom_shape").to_s,
104
+ params: deep_dup(Hash(values[:params] || values["params"] || {})),
105
+ param_schema: Array(values[:param_schema] || values["param_schema"]).map { |entry| deep_dup(entry) },
106
+ shape_indices: (start_index...(start_index + count)).to_a
107
+ }
108
+ end
109
+
110
+ def apply_custom_shape_overrides!(params, layer_name:, custom_shape_overrides:)
111
+ layer_overrides = custom_shape_layer_overrides(custom_shape_overrides, layer_name)
112
+ return if layer_overrides.empty?
113
+
114
+ descriptors = Array(params[:custom_shapes] || params["custom_shapes"])
115
+ layer_overrides.each do |index, values|
116
+ descriptor = descriptors[Integer(index)]
117
+ next unless descriptor && values.is_a?(Hash)
118
+
119
+ descriptor[:params] ||= {}
120
+ values.each do |param_name, value|
121
+ key = param_name.to_sym
122
+ descriptor[:params][key] = value
123
+ end
124
+ rescue ArgumentError, TypeError
125
+ next
126
+ end
127
+ end
128
+
129
+ def custom_shape_layer_overrides(overrides, layer_name)
130
+ values = Hash(overrides)
131
+ name = layer_name.to_s
132
+ Hash(values[name] || values[layer_name.to_sym] || {})
133
+ rescue TypeError
134
+ {}
135
+ end
136
+
137
+ def apply_custom_shape_attributes!(primitive, descriptor)
138
+ style = Hash(descriptor[:style] || {})
139
+ style.each do |key, value|
140
+ symbol_key = key.to_sym
141
+ if symbol_key == :opacity && primitive.key?(:opacity)
142
+ primitive[:opacity] = numeric(style[:opacity] || style["opacity"], :opacity) * numeric(primitive[:opacity], :opacity)
143
+ else
144
+ primitive[symbol_key] = deep_dup(value) unless primitive.key?(symbol_key)
145
+ end
146
+ end
147
+
148
+ transform = Hash(descriptor[:transform] || {})
149
+ primitive[:transform] = compose_shape_transform(transform, primitive[:transform]) unless transform.empty?
150
+ primitive
151
+ end
152
+
60
153
  def assign_nested_param(container, path, value)
61
154
  key = path.shift
62
155
  if path.empty?
@@ -65,6 +158,7 @@ module Vizcore
65
158
  end
66
159
 
67
160
  next_container = nested_value(container, key)
161
+ next_container = create_nested_container(container, key, path.first) if next_container.nil?
68
162
  return unless next_container
69
163
 
70
164
  assign_nested_param(next_container, path, value)
@@ -77,6 +171,13 @@ module Vizcore
77
171
  nil
78
172
  end
79
173
 
174
+ def create_nested_container(container, key, next_key)
175
+ return unless container.is_a?(Hash)
176
+
177
+ value = integer_key?(next_key) ? [] : {}
178
+ container[key.to_sym] = value
179
+ end
180
+
80
181
  def assign_nested_value(container, key, value)
81
182
  if container.is_a?(Array) && integer_key?(key)
82
183
  container[key.to_i] = value
@@ -89,6 +190,46 @@ module Vizcore
89
190
  value.match?(/\A\d+\z/)
90
191
  end
91
192
 
193
+ def compose_shape_transform(parent, child)
194
+ return deep_dup(child || {}) unless parent
195
+
196
+ child ||= {}
197
+ output = deep_dup(parent)
198
+ output[:translate] = add_shape_xy(parent[:translate], child[:translate]) if child.key?(:translate)
199
+ output[:origin] = child[:origin] if child.key?(:origin)
200
+ output[:rotate] = numeric(parent[:rotate] || 0, :rotate) + numeric(child[:rotate] || 0, :rotate) if child.key?(:rotate)
201
+ output[:scale] = multiply_shape_scale(parent[:scale], child[:scale]) if child.key?(:scale)
202
+ output
203
+ end
204
+
205
+ def add_shape_xy(parent, child)
206
+ parent ||= {}
207
+ child ||= {}
208
+ {
209
+ x: numeric(parent[:x] || parent["x"] || 0, :"translate.x") + numeric(child[:x] || child["x"] || 0, :"translate.x"),
210
+ y: numeric(parent[:y] || parent["y"] || 0, :"translate.y") + numeric(child[:y] || child["y"] || 0, :"translate.y")
211
+ }
212
+ end
213
+
214
+ def multiply_shape_scale(parent, child)
215
+ parent = shape_scale_pair(parent)
216
+ child = shape_scale_pair(child)
217
+ { x: parent[:x] * child[:x], y: parent[:y] * child[:y] }
218
+ end
219
+
220
+ def shape_scale_pair(value)
221
+ return { x: numeric(value[:x] || value["x"] || 1, :"scale.x"), y: numeric(value[:y] || value["y"] || 1, :"scale.y") } if value.is_a?(Hash)
222
+
223
+ scale = numeric(value || 1, :scale)
224
+ { x: scale, y: scale }
225
+ end
226
+
227
+ def numeric(value, name)
228
+ Float(value)
229
+ rescue ArgumentError, TypeError
230
+ raise ArgumentError, "param #{name} must be numeric"
231
+ end
232
+
92
233
  def resolve_source_value(source, audio)
93
234
  case source[:kind]&.to_sym
94
235
  when :amplitude
@@ -193,6 +334,19 @@ module Vizcore
193
334
  Array(scene_layers).map { |layer| deep_symbolize(layer) }
194
335
  end
195
336
 
337
+ def deep_dup(value)
338
+ case value
339
+ when Hash
340
+ value.each_with_object({}) do |(key, entry), output|
341
+ output[key] = deep_dup(entry)
342
+ end
343
+ when Array
344
+ value.map { |entry| deep_dup(entry) }
345
+ else
346
+ value
347
+ end
348
+ end
349
+
196
350
  def deep_symbolize(value)
197
351
  case value
198
352
  when Hash
@@ -151,10 +151,12 @@ module Vizcore
151
151
  aliases: %i[shapes shape_layer],
152
152
  params: COMMON_PARAMS.merge(
153
153
  shapes: "Array<Hash>",
154
+ shape_schema_version: "Integer",
155
+ units: "Symbol",
154
156
  color_shift: "Float"
155
157
  ),
156
- mappable_params: %i[color_shift opacity],
157
- description: "Declarative 2D circle and line primitives rendered by the browser."
158
+ mappable_params: %i[color_shift opacity shapes],
159
+ description: "Declarative and Ruby-generated 2D circle, line, rect, polygon, polyline, path, and star primitives rendered by the browser."
158
160
  ),
159
161
  Capability.new(
160
162
  type: :mesh,
@@ -22,6 +22,7 @@ module Vizcore
22
22
  @input_manager.start
23
23
  @capture_size = capture_size
24
24
  @pipeline = build_pipeline
25
+ @frame_count = 0
25
26
  self
26
27
  end
27
28
 
@@ -30,7 +31,13 @@ module Vizcore
30
31
  ensure_started!
31
32
 
32
33
  audio = @pipeline.call(@input_manager.capture_frame(@capture_size))
33
- layers = Vizcore::DSL::MappingResolver.new.resolve_layers(scene_layers: @scene[:layers], audio: audio)
34
+ @frame_count += 1
35
+ layers = Vizcore::DSL::MappingResolver.new.resolve_layers(
36
+ scene_layers: @scene[:layers],
37
+ audio: audio,
38
+ time: frame_time,
39
+ frame: @frame_count
40
+ )
34
41
 
35
42
  {
36
43
  scene: { name: @scene[:name], layers: layers },
@@ -82,6 +89,12 @@ module Vizcore
82
89
  )
83
90
  end
84
91
 
92
+ def frame_time
93
+ return 0.0 unless @frame_rate
94
+
95
+ (@frame_count - 1).fdiv(@frame_rate)
96
+ end
97
+
85
98
  def audio_normalize_settings
86
99
  Hash(@definition[:analysis] || {})[:audio_normalize]
87
100
  rescue StandardError