vizcore 1.0.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/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 +50 -2
- data/frontend/src/audio-inspector.js +9 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/live-controls.js +219 -7
- data/frontend/src/main.js +703 -45
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/midi-learn.js +22 -2
- data/frontend/src/performance-monitor.js +137 -1
- data/frontend/src/renderer/engine.js +401 -11
- data/frontend/src/renderer/layer-manager.js +490 -75
- 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/shape-editor-controls.js +157 -0
- data/frontend/src/visuals/geometry.js +425 -27
- 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 +488 -0
- data/frontend/src/visuals/spectrogram-renderer.js +14 -0
- data/frontend/src/visuals/svg-arc.js +104 -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 +65 -9
- 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 +573 -33
- 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 +1072 -21
- 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 +549 -13
- 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 +5 -2
- 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 +190 -12
- data/lib/vizcore/renderer/scene_serializer.rb +38 -0
- data/lib/vizcore/renderer/snapshot.rb +4 -3
- data/lib/vizcore/renderer/snapshot_renderer.rb +641 -23
- data/lib/vizcore/scene_trust.rb +31 -0
- data/lib/vizcore/server/frame_broadcaster.rb +513 -18
- data/lib/vizcore/server/rack_app.rb +151 -4
- data/lib/vizcore/server/runner.rb +697 -82
- data/lib/vizcore/server/websocket_handler.rb +236 -14
- data/lib/vizcore/server.rb +21 -0
- data/lib/vizcore/shape.rb +742 -0
- data/lib/vizcore/sync/osc_message.rb +66 -9
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +34 -0
- data/scripts/browser_capture.mjs +31 -2
- data/sig/vizcore.rbs +154 -4
- metadata +29 -3
|
@@ -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],
|
|
@@ -16,9 +19,10 @@ module Vizcore
|
|
|
16
19
|
[250, 204, 21]
|
|
17
20
|
].freeze
|
|
18
21
|
|
|
19
|
-
def initialize(width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT)
|
|
22
|
+
def initialize(width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, transparent: false)
|
|
20
23
|
@width = normalize_dimension(width)
|
|
21
24
|
@height = normalize_dimension(height)
|
|
25
|
+
@transparent = !!transparent
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
attr_reader :width, :height
|
|
@@ -27,8 +31,8 @@ module Vizcore
|
|
|
27
31
|
# @param audio [Hash]
|
|
28
32
|
# @return [String] PNG bytes
|
|
29
33
|
def render(scene:, audio:)
|
|
30
|
-
canvas = Canvas.new(width: width, height: height)
|
|
31
|
-
canvas.fill_gradient(background_top(audio), background_bottom(audio))
|
|
34
|
+
canvas = Canvas.new(width: width, height: height, transparent: @transparent)
|
|
35
|
+
canvas.fill_gradient(background_top(audio), background_bottom(audio)) unless @transparent
|
|
32
36
|
layers = Array(scene[:layers] || scene["layers"])
|
|
33
37
|
layers = [default_layer] if layers.empty?
|
|
34
38
|
layers.each_with_index { |layer, index| render_layer(canvas, layer, audio, index) }
|
|
@@ -156,6 +160,7 @@ module Vizcore
|
|
|
156
160
|
def render_shape_layer(canvas, layer, audio, color)
|
|
157
161
|
params = Hash(layer[:params] || layer["params"] || {})
|
|
158
162
|
shapes = Array(params[:shapes] || params["shapes"])
|
|
163
|
+
context = shape_coordinate_context(params)
|
|
159
164
|
pulse = clamp(audio[:beat_pulse])
|
|
160
165
|
alpha = 0.58 + pulse * 0.24
|
|
161
166
|
|
|
@@ -163,34 +168,522 @@ module Vizcore
|
|
|
163
168
|
shape_hash = Hash(shape)
|
|
164
169
|
case (shape_hash[:kind] || shape_hash["kind"]).to_s
|
|
165
170
|
when "circle"
|
|
166
|
-
render_circle_shape(canvas, shape_hash, color, alpha)
|
|
171
|
+
render_circle_shape(canvas, shape_hash, color, alpha, context)
|
|
167
172
|
when "line"
|
|
168
|
-
render_line_shape(canvas, shape_hash, color, alpha)
|
|
173
|
+
render_line_shape(canvas, shape_hash, color, alpha, context)
|
|
174
|
+
when "rect"
|
|
175
|
+
render_rect_shape(canvas, shape_hash, color, alpha, context)
|
|
176
|
+
when "polygon", "polyline"
|
|
177
|
+
render_polygon_shape(canvas, shape_hash, color, alpha, context)
|
|
178
|
+
when "path"
|
|
179
|
+
render_path_shape(canvas, shape_hash, color, alpha, context)
|
|
180
|
+
when "star"
|
|
181
|
+
render_star_shape(canvas, shape_hash, color, alpha, context)
|
|
169
182
|
end
|
|
170
183
|
end
|
|
171
184
|
rescue ArgumentError, TypeError
|
|
172
185
|
nil
|
|
173
186
|
end
|
|
174
187
|
|
|
175
|
-
def render_circle_shape(canvas, shape, color, alpha)
|
|
188
|
+
def render_circle_shape(canvas, shape, color, alpha, context)
|
|
176
189
|
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
|
|
190
|
+
radius = shape_length(shape[:radius] || shape["radius"] || 100, context, :radius)
|
|
191
|
+
center = shape_point(shape[:x] || shape["x"] || 0, shape[:y] || shape["y"] || 0, context)
|
|
182
192
|
|
|
183
193
|
count.times do |index|
|
|
184
|
-
|
|
194
|
+
ring_radius = radius * ((index + 1).to_f / count)
|
|
195
|
+
render_polyline_shape(canvas, circle_points(center, ring_radius), shape, color, alpha, context, closed: true)
|
|
185
196
|
end
|
|
186
197
|
end
|
|
187
198
|
|
|
188
|
-
def render_line_shape(canvas, shape, color, alpha)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
199
|
+
def render_line_shape(canvas, shape, color, alpha, context)
|
|
200
|
+
defaults = context[:units] == :legacy || context[:units] == :ndc ? [-0.8, 0, 0.8, 0] : [-100, 0, 100, 0]
|
|
201
|
+
from = shape_point(shape[:x1] || shape["x1"] || defaults[0], shape[:y1] || shape["y1"] || defaults[1], context)
|
|
202
|
+
to = shape_point(shape[:x2] || shape["x2"] || defaults[2], shape[:y2] || shape["y2"] || defaults[3], context)
|
|
203
|
+
draw_shape_segment(canvas, from, to, shape, color, alpha, context)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def render_rect_shape(canvas, shape, color, alpha, context)
|
|
207
|
+
center = shape_point(shape[:x] || shape["x"] || 0, shape[:y] || shape["y"] || 0, context)
|
|
208
|
+
half_width = shape_length(shape[:width] || shape["width"] || 100, context, :x) / 2.0
|
|
209
|
+
half_height = shape_length(shape[:height] || shape["height"] || 100, context, :y) / 2.0
|
|
210
|
+
points = [
|
|
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
|
+
[center[0] - half_width, center[1] + half_height]
|
|
215
|
+
]
|
|
216
|
+
render_polyline_shape(canvas, points, shape, color, alpha, context, closed: true)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def render_polygon_shape(canvas, shape, color, alpha, context)
|
|
220
|
+
points = Array(shape[:points] || shape["points"]).filter_map do |point|
|
|
221
|
+
values = Array(point)
|
|
222
|
+
next if values.length < 2
|
|
223
|
+
|
|
224
|
+
shape_point(values[0], values[1], context)
|
|
225
|
+
end
|
|
226
|
+
closed = (shape[:kind] || shape["kind"]).to_s == "polygon" ? shape.fetch(:closed, shape.fetch("closed", true)) : false
|
|
227
|
+
render_polyline_shape(canvas, points, shape, color, alpha, context, closed: closed)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def render_star_shape(canvas, shape, color, alpha, context)
|
|
231
|
+
tips = [[Integer(shape[:points] || shape["points"] || 5), 3].max, 128].min
|
|
232
|
+
center = shape_point(shape[:x] || shape["x"] || 0, shape[:y] || shape["y"] || 0, context)
|
|
233
|
+
radius = shape_length(shape[:radius] || shape["radius"] || 100, context, :radius)
|
|
234
|
+
inner_radius = shape_length(shape[:inner_radius] || shape["inner_radius"] || Float(shape[:radius] || shape["radius"] || 100) * 0.5, context, :radius)
|
|
235
|
+
rotation = Float(shape[:rotation] || shape["rotation"] || -90) * Math::PI / 180.0
|
|
236
|
+
points = (tips * 2).times.map do |index|
|
|
237
|
+
angle = rotation + (index.to_f / (tips * 2)) * Math::PI * 2
|
|
238
|
+
point_radius = index.even? ? radius : inner_radius
|
|
239
|
+
[center[0] + Math.cos(angle) * point_radius, center[1] - Math.sin(angle) * point_radius]
|
|
240
|
+
end
|
|
241
|
+
render_polyline_shape(canvas, points, shape, color, alpha, context, closed: true)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def render_path_shape(canvas, shape, color, alpha, context)
|
|
245
|
+
detail = [[Integer(shape[:detail] || shape["detail"] || 32), 4].max, 128].min
|
|
246
|
+
tolerance = path_tolerance(shape)
|
|
247
|
+
segment_budget = { remaining: path_segment_limit(shape) }
|
|
248
|
+
current = nil
|
|
249
|
+
subpath_start = nil
|
|
250
|
+
Array(shape[:commands] || shape["commands"]).each do |entry|
|
|
251
|
+
command, *values = Array(entry)
|
|
252
|
+
values = values.map { |value| Float(value) }
|
|
253
|
+
case command.to_s.upcase
|
|
254
|
+
when "M"
|
|
255
|
+
current = values.first(2)
|
|
256
|
+
subpath_start = current
|
|
257
|
+
when "L"
|
|
258
|
+
next unless current && values.length >= 2
|
|
259
|
+
|
|
260
|
+
current = draw_raw_path_segment(canvas, current, values.first(2), shape, color, alpha, context, segment_budget)
|
|
261
|
+
when "H"
|
|
262
|
+
next unless current && values.length >= 1
|
|
263
|
+
|
|
264
|
+
current = draw_raw_path_segment(canvas, current, [values[0], current[1]], shape, color, alpha, context, segment_budget)
|
|
265
|
+
when "V"
|
|
266
|
+
next unless current && values.length >= 1
|
|
267
|
+
|
|
268
|
+
current = draw_raw_path_segment(canvas, current, [current[0], values[0]], shape, color, alpha, context, segment_budget)
|
|
269
|
+
when "Q"
|
|
270
|
+
next unless current && values.length >= 4
|
|
271
|
+
|
|
272
|
+
current = draw_quadratic_path(canvas, current, values, detail, tolerance, shape, color, alpha, context, segment_budget)
|
|
273
|
+
when "C"
|
|
274
|
+
next unless current && values.length >= 6
|
|
275
|
+
|
|
276
|
+
current = draw_cubic_path(canvas, current, values, detail, tolerance, shape, color, alpha, context, segment_budget)
|
|
277
|
+
when "A"
|
|
278
|
+
next unless current && values.length >= 7
|
|
279
|
+
|
|
280
|
+
current = draw_arc_path(canvas, current, values, detail, shape, color, alpha, context, segment_budget)
|
|
281
|
+
when "Z"
|
|
282
|
+
if current && subpath_start
|
|
283
|
+
current = draw_raw_path_segment(canvas, current, subpath_start, shape, color, alpha, context, segment_budget)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def render_polyline_shape(canvas, points, shape, color, alpha, context, closed:)
|
|
290
|
+
return if points.length < 2
|
|
291
|
+
|
|
292
|
+
points.each_cons(2) { |from, to| draw_shape_segment(canvas, from, to, shape, color, alpha, context) }
|
|
293
|
+
draw_shape_segment(canvas, points.last, points.first, shape, color, alpha, context) if closed && points.length > 2
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def draw_raw_path_segment(canvas, from, to, shape, color, alpha, context, segment_budget = nil)
|
|
297
|
+
return to if segment_budget && segment_budget[:remaining] <= 0
|
|
298
|
+
|
|
299
|
+
draw_shape_segment(canvas, shape_point(from[0], from[1], context), shape_point(to[0], to[1], context), shape, color, alpha, context)
|
|
300
|
+
segment_budget[:remaining] -= 1 if segment_budget
|
|
301
|
+
to
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def draw_quadratic_path(canvas, current, values, detail, tolerance, shape, color, alpha, context, segment_budget = nil)
|
|
305
|
+
previous = current
|
|
306
|
+
control = values.first(2)
|
|
307
|
+
endpoint = values.last(2)
|
|
308
|
+
if tolerance
|
|
309
|
+
draw_adaptive_quadratic_path(canvas, current, control, endpoint, tolerance, shape, color, alpha, context, segment_budget)
|
|
310
|
+
return endpoint
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
1.upto(detail) do |step|
|
|
314
|
+
break if segment_budget && segment_budget[:remaining] <= 0
|
|
315
|
+
|
|
316
|
+
t = step.to_f / detail
|
|
317
|
+
point = [
|
|
318
|
+
quadratic_point(current[0], control[0], endpoint[0], t),
|
|
319
|
+
quadratic_point(current[1], control[1], endpoint[1], t)
|
|
320
|
+
]
|
|
321
|
+
draw_raw_path_segment(canvas, previous, point, shape, color, alpha, context, segment_budget)
|
|
322
|
+
previous = point
|
|
323
|
+
end
|
|
324
|
+
endpoint
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def draw_cubic_path(canvas, current, values, detail, tolerance, shape, color, alpha, context, segment_budget = nil)
|
|
328
|
+
previous = current
|
|
329
|
+
c1 = values[0, 2]
|
|
330
|
+
c2 = values[2, 2]
|
|
331
|
+
endpoint = values[4, 2]
|
|
332
|
+
if tolerance
|
|
333
|
+
draw_adaptive_cubic_path(canvas, current, c1, c2, endpoint, tolerance, shape, color, alpha, context, segment_budget)
|
|
334
|
+
return endpoint
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
1.upto(detail) do |step|
|
|
338
|
+
break if segment_budget && segment_budget[:remaining] <= 0
|
|
339
|
+
|
|
340
|
+
t = step.to_f / detail
|
|
341
|
+
point = [
|
|
342
|
+
cubic_point(current[0], c1[0], c2[0], endpoint[0], t),
|
|
343
|
+
cubic_point(current[1], c1[1], c2[1], endpoint[1], t)
|
|
344
|
+
]
|
|
345
|
+
draw_raw_path_segment(canvas, previous, point, shape, color, alpha, context, segment_budget)
|
|
346
|
+
previous = point
|
|
347
|
+
end
|
|
348
|
+
endpoint
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def draw_arc_path(canvas, current, values, detail, shape, color, alpha, context, segment_budget = nil)
|
|
352
|
+
endpoint = values[5, 2]
|
|
353
|
+
arc = svg_arc_description(
|
|
354
|
+
from: current,
|
|
355
|
+
to: endpoint,
|
|
356
|
+
rx: values[0],
|
|
357
|
+
ry: values[1],
|
|
358
|
+
x_axis_rotation: values[2],
|
|
359
|
+
large_arc: arc_flag(values[3]),
|
|
360
|
+
sweep: arc_flag(values[4])
|
|
361
|
+
)
|
|
362
|
+
unless arc
|
|
363
|
+
return draw_raw_path_segment(canvas, current, endpoint, shape, color, alpha, context, segment_budget)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
previous = current
|
|
367
|
+
segments = svg_arc_segment_count(arc, detail)
|
|
368
|
+
1.upto(segments) do |step|
|
|
369
|
+
break if segment_budget && segment_budget[:remaining] <= 0
|
|
370
|
+
|
|
371
|
+
point = svg_arc_point(arc, step.to_f / segments)
|
|
372
|
+
draw_raw_path_segment(canvas, previous, point, shape, color, alpha, context, segment_budget)
|
|
373
|
+
previous = point
|
|
374
|
+
end
|
|
375
|
+
endpoint
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def path_segment_limit(shape)
|
|
379
|
+
raw_value = shape[:max_segments] || shape["max_segments"] || PATH_DEFAULT_MAX_SEGMENTS
|
|
380
|
+
[[Integer(raw_value), 1].max, PATH_HARD_MAX_SEGMENTS].min
|
|
381
|
+
rescue ArgumentError, TypeError
|
|
382
|
+
PATH_DEFAULT_MAX_SEGMENTS
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def path_tolerance(shape)
|
|
386
|
+
return unless shape.key?(:tolerance) || shape.key?("tolerance")
|
|
387
|
+
|
|
388
|
+
value = Float(shape[:tolerance] || shape["tolerance"])
|
|
389
|
+
value if value.finite? && value >= 0
|
|
390
|
+
rescue ArgumentError, TypeError
|
|
391
|
+
nil
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def draw_adaptive_quadratic_path(canvas, from, control, to, tolerance, shape, color, alpha, context, segment_budget, depth = 0)
|
|
395
|
+
return if segment_budget && segment_budget[:remaining] <= 0
|
|
396
|
+
|
|
397
|
+
if depth >= PATH_MAX_RECURSION || point_line_distance(control, from, to) <= tolerance
|
|
398
|
+
draw_raw_path_segment(canvas, from, to, shape, color, alpha, context, segment_budget)
|
|
399
|
+
return
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
left_control = midpoint(from, control)
|
|
403
|
+
right_control = midpoint(control, to)
|
|
404
|
+
center = midpoint(left_control, right_control)
|
|
405
|
+
draw_adaptive_quadratic_path(canvas, from, left_control, center, tolerance, shape, color, alpha, context, segment_budget, depth + 1)
|
|
406
|
+
draw_adaptive_quadratic_path(canvas, center, right_control, to, tolerance, shape, color, alpha, context, segment_budget, depth + 1)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def draw_adaptive_cubic_path(canvas, from, c1, c2, to, tolerance, shape, color, alpha, context, segment_budget, depth = 0)
|
|
410
|
+
return if segment_budget && segment_budget[:remaining] <= 0
|
|
411
|
+
|
|
412
|
+
flatness = [point_line_distance(c1, from, to), point_line_distance(c2, from, to)].max
|
|
413
|
+
if depth >= PATH_MAX_RECURSION || flatness <= tolerance
|
|
414
|
+
draw_raw_path_segment(canvas, from, to, shape, color, alpha, context, segment_budget)
|
|
415
|
+
return
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
p01 = midpoint(from, c1)
|
|
419
|
+
p12 = midpoint(c1, c2)
|
|
420
|
+
p23 = midpoint(c2, to)
|
|
421
|
+
p012 = midpoint(p01, p12)
|
|
422
|
+
p123 = midpoint(p12, p23)
|
|
423
|
+
center = midpoint(p012, p123)
|
|
424
|
+
draw_adaptive_cubic_path(canvas, from, p01, p012, center, tolerance, shape, color, alpha, context, segment_budget, depth + 1)
|
|
425
|
+
draw_adaptive_cubic_path(canvas, center, p123, p23, to, tolerance, shape, color, alpha, context, segment_budget, depth + 1)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def midpoint(from, to)
|
|
429
|
+
[(from[0] + to[0]) * 0.5, (from[1] + to[1]) * 0.5]
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def point_line_distance(point, from, to)
|
|
433
|
+
dx = to[0] - from[0]
|
|
434
|
+
dy = to[1] - from[1]
|
|
435
|
+
length = Math.sqrt((dx * dx) + (dy * dy))
|
|
436
|
+
return Math.sqrt(((point[0] - from[0])**2) + ((point[1] - from[1])**2)) if length <= 0
|
|
437
|
+
|
|
438
|
+
((dy * point[0]) - (dx * point[1]) + (to[0] * from[1]) - (to[1] * from[0])).abs / length
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def draw_shape_segment(canvas, from, to, shape, color, alpha, context)
|
|
442
|
+
from = apply_shape_transform(from, shape, context)
|
|
443
|
+
to = apply_shape_transform(to, shape, context)
|
|
444
|
+
canvas.draw_line(from[0], from[1], to[0], to[1], color, alpha: alpha * shape_opacity(shape))
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def shape_coordinate_context(params)
|
|
448
|
+
units = (params[:units] || params["units"]).to_s.strip.downcase
|
|
449
|
+
version = Integer(params[:shape_schema_version] || params["shape_schema_version"] || 1)
|
|
450
|
+
{ units: (units.empty? ? (version >= 2 ? :logical : :legacy) : units.to_sym) }
|
|
451
|
+
rescue ArgumentError, TypeError
|
|
452
|
+
{ units: :legacy }
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def shape_point(x, y, context)
|
|
456
|
+
[shape_coordinate(x, context, :x), shape_coordinate(y, context, :y)]
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def shape_coordinate(value, context, axis)
|
|
460
|
+
numeric = Float(value || 0)
|
|
461
|
+
case context[:units]
|
|
462
|
+
when :ndc
|
|
463
|
+
axis == :x ? width * 0.5 + numeric * width * 0.5 : height * 0.5 - numeric * height * 0.5
|
|
464
|
+
when :logical, :center, :center_origin, :px
|
|
465
|
+
axis == :x ? width * 0.5 + numeric : height * 0.5 - numeric
|
|
466
|
+
when :screen, :canvas, :viewport
|
|
467
|
+
numeric
|
|
468
|
+
else
|
|
469
|
+
legacy_shape_coordinate(numeric, axis)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def legacy_shape_coordinate(value, axis)
|
|
474
|
+
return axis == :x ? width * 0.5 + value * width * 0.5 : height * 0.5 - value * height * 0.5 if value.abs <= 1.5
|
|
475
|
+
|
|
476
|
+
value
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def shape_length(value, context, _axis)
|
|
480
|
+
numeric = Float(value || 0).abs
|
|
481
|
+
return numeric * [width, height].min * 0.5 if context[:units] == :ndc || numeric <= 2
|
|
482
|
+
|
|
483
|
+
numeric
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def circle_points(center, radius)
|
|
487
|
+
segments = 96
|
|
488
|
+
segments.times.map do |index|
|
|
489
|
+
angle = (index.to_f / segments) * Math::PI * 2
|
|
490
|
+
[center[0] + Math.cos(angle) * radius, center[1] + Math.sin(angle) * radius]
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def apply_shape_transform(point, shape, context)
|
|
495
|
+
transform = shape_transform(shape, context)
|
|
496
|
+
shifted_x = (point[0] - transform[:origin][0]) * transform[:scale][:x]
|
|
497
|
+
shifted_y = (point[1] - transform[:origin][1]) * transform[:scale][:y]
|
|
498
|
+
radians = -transform[:rotate] * Math::PI / 180.0
|
|
499
|
+
cos = Math.cos(radians)
|
|
500
|
+
sin = Math.sin(radians)
|
|
501
|
+
rotated_x = shifted_x * cos - shifted_y * sin
|
|
502
|
+
rotated_y = shifted_x * sin + shifted_y * cos
|
|
503
|
+
|
|
504
|
+
[
|
|
505
|
+
rotated_x + transform[:origin][0] + transform[:translate][:x],
|
|
506
|
+
rotated_y + transform[:origin][1] + transform[:translate][:y]
|
|
507
|
+
]
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def shape_transform(shape, context)
|
|
511
|
+
transform = Hash(shape[:transform] || shape["transform"] || {})
|
|
512
|
+
{
|
|
513
|
+
translate: shape_vector_pair(shape_hash_value(transform, :translate) || shape_hash_value(shape, :translate), context),
|
|
514
|
+
origin: shape_origin_pair(shape_hash_value(transform, :origin) || shape_hash_value(shape, :origin), context),
|
|
515
|
+
rotate: Float(shape_hash_value(transform, :rotate) || shape_hash_value(shape, :rotate) || shape_hash_value(shape, :rotation) || 0),
|
|
516
|
+
scale: shape_scale(shape_hash_value(transform, :scale) || shape_hash_value(shape, :scale))
|
|
517
|
+
}
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def shape_vector_pair(value, context)
|
|
521
|
+
if value.is_a?(Array)
|
|
522
|
+
return { x: shape_vector(value[0], context, :x), y: shape_vector(value[1], context, :y) }
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
values = value.is_a?(Hash) ? value : {}
|
|
526
|
+
{ x: shape_vector(shape_hash_value(values, :x) || 0, context, :x), y: shape_vector(shape_hash_value(values, :y) || 0, context, :y) }
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def shape_origin_pair(value, context)
|
|
530
|
+
if value.is_a?(Array)
|
|
531
|
+
return shape_point(value[0], value[1], context)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
values = value.is_a?(Hash) ? value : {}
|
|
535
|
+
shape_point(shape_hash_value(values, :x) || 0, shape_hash_value(values, :y) || 0, context)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def shape_vector(value, context, axis)
|
|
539
|
+
numeric = Float(value || 0)
|
|
540
|
+
case context[:units]
|
|
541
|
+
when :ndc
|
|
542
|
+
axis == :x ? numeric * width * 0.5 : -numeric * height * 0.5
|
|
543
|
+
else
|
|
544
|
+
axis == :x ? numeric : -numeric
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def shape_scale(value)
|
|
549
|
+
if value.is_a?(Hash)
|
|
550
|
+
return {
|
|
551
|
+
x: Float(shape_hash_value(value, :x) || 1).clamp(-8.0, 8.0),
|
|
552
|
+
y: Float(shape_hash_value(value, :y) || 1).clamp(-8.0, 8.0)
|
|
553
|
+
}
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
scale = Float(value || 1).clamp(-8.0, 8.0)
|
|
557
|
+
{ x: scale, y: scale }
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def shape_opacity(shape)
|
|
561
|
+
Float(shape[:opacity] || shape["opacity"] || 1).clamp(0.0, 1.0)
|
|
562
|
+
rescue ArgumentError, TypeError
|
|
563
|
+
1.0
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def shape_hash_value(hash, key)
|
|
567
|
+
hash[key] || hash[key.to_s]
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def quadratic_point(from, control, to, t)
|
|
571
|
+
inv = 1.0 - t
|
|
572
|
+
inv * inv * from + 2 * inv * t * control + t * t * to
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def cubic_point(from, c1, c2, to, t)
|
|
576
|
+
inv = 1.0 - t
|
|
577
|
+
inv * inv * inv * from + 3 * inv * inv * t * c1 + 3 * inv * t * t * c2 + t * t * t * to
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def svg_arc_description(from:, to:, rx:, ry:, x_axis_rotation:, large_arc:, sweep:)
|
|
581
|
+
return if same_point?(from, to)
|
|
582
|
+
|
|
583
|
+
radius_x = Float(rx || 0).abs
|
|
584
|
+
radius_y = Float(ry || 0).abs
|
|
585
|
+
return if radius_x <= 0 || radius_y <= 0
|
|
586
|
+
|
|
587
|
+
rotation = Float(x_axis_rotation || 0) * Math::PI / 180.0
|
|
588
|
+
cos = Math.cos(rotation)
|
|
589
|
+
sin = Math.sin(rotation)
|
|
590
|
+
dx = (from[0] - to[0]) / 2.0
|
|
591
|
+
dy = (from[1] - to[1]) / 2.0
|
|
592
|
+
x1p = cos * dx + sin * dy
|
|
593
|
+
y1p = -sin * dx + cos * dy
|
|
594
|
+
|
|
595
|
+
scale = (x1p * x1p / (radius_x * radius_x)) + (y1p * y1p / (radius_y * radius_y))
|
|
596
|
+
if scale > 1
|
|
597
|
+
multiplier = Math.sqrt(scale)
|
|
598
|
+
radius_x *= multiplier
|
|
599
|
+
radius_y *= multiplier
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
center = svg_arc_center(
|
|
603
|
+
from: from,
|
|
604
|
+
to: to,
|
|
605
|
+
radius_x: radius_x,
|
|
606
|
+
radius_y: radius_y,
|
|
607
|
+
x1p: x1p,
|
|
608
|
+
y1p: y1p,
|
|
609
|
+
rotation_cos: cos,
|
|
610
|
+
rotation_sin: sin,
|
|
611
|
+
large_arc: large_arc,
|
|
612
|
+
sweep: sweep
|
|
613
|
+
)
|
|
614
|
+
return unless center
|
|
615
|
+
|
|
616
|
+
start_vector = [(x1p - center[:cxp]) / radius_x, (y1p - center[:cyp]) / radius_y]
|
|
617
|
+
end_vector = [(-x1p - center[:cxp]) / radius_x, (-y1p - center[:cyp]) / radius_y]
|
|
618
|
+
start_angle = vector_angle([1.0, 0.0], start_vector)
|
|
619
|
+
delta_angle = vector_angle(start_vector, end_vector)
|
|
620
|
+
delta_angle -= Math::PI * 2 if !sweep && delta_angle.positive?
|
|
621
|
+
delta_angle += Math::PI * 2 if sweep && delta_angle.negative?
|
|
622
|
+
|
|
623
|
+
{
|
|
624
|
+
cx: center[:cx],
|
|
625
|
+
cy: center[:cy],
|
|
626
|
+
rx: radius_x,
|
|
627
|
+
ry: radius_y,
|
|
628
|
+
rotation: rotation,
|
|
629
|
+
start_angle: start_angle,
|
|
630
|
+
delta_angle: delta_angle
|
|
631
|
+
}
|
|
632
|
+
rescue ArgumentError, TypeError
|
|
633
|
+
nil
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def svg_arc_center(from:, to:, radius_x:, radius_y:, x1p:, y1p:, rotation_cos:, rotation_sin:, large_arc:, sweep:)
|
|
637
|
+
rx2 = radius_x * radius_x
|
|
638
|
+
ry2 = radius_y * radius_y
|
|
639
|
+
x1p2 = x1p * x1p
|
|
640
|
+
y1p2 = y1p * y1p
|
|
641
|
+
denominator = rx2 * y1p2 + ry2 * x1p2
|
|
642
|
+
return if denominator.zero?
|
|
643
|
+
|
|
644
|
+
numerator = [rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2, 0.0].max
|
|
645
|
+
sign = large_arc == sweep ? -1.0 : 1.0
|
|
646
|
+
coefficient = sign * Math.sqrt(numerator / denominator)
|
|
647
|
+
cxp = coefficient * ((radius_x * y1p) / radius_y)
|
|
648
|
+
cyp = coefficient * (-(radius_y * x1p) / radius_x)
|
|
649
|
+
{
|
|
650
|
+
cxp: cxp,
|
|
651
|
+
cyp: cyp,
|
|
652
|
+
cx: rotation_cos * cxp - rotation_sin * cyp + (from[0] + to[0]) / 2.0,
|
|
653
|
+
cy: rotation_sin * cxp + rotation_cos * cyp + (from[1] + to[1]) / 2.0
|
|
654
|
+
}
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def svg_arc_point(arc, progress)
|
|
658
|
+
angle = arc[:start_angle] + arc[:delta_angle] * progress
|
|
659
|
+
cos_rotation = Math.cos(arc[:rotation])
|
|
660
|
+
sin_rotation = Math.sin(arc[:rotation])
|
|
661
|
+
x = Math.cos(angle) * arc[:rx]
|
|
662
|
+
y = Math.sin(angle) * arc[:ry]
|
|
663
|
+
[
|
|
664
|
+
arc[:cx] + cos_rotation * x - sin_rotation * y,
|
|
665
|
+
arc[:cy] + sin_rotation * x + cos_rotation * y
|
|
666
|
+
]
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def svg_arc_segment_count(arc, detail)
|
|
670
|
+
[((arc[:delta_angle].abs / (Math::PI * 2)) * detail).ceil, 1].max
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def vector_angle(from, to)
|
|
674
|
+
cross = from[0] * to[1] - from[1] * to[0]
|
|
675
|
+
dot = from[0] * to[0] + from[1] * to[1]
|
|
676
|
+
Math.atan2(cross, dot)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def same_point?(from, to)
|
|
680
|
+
(from[0] - to[0]).abs < 1e-9 && (from[1] - to[1]).abs < 1e-9
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def arc_flag(value)
|
|
684
|
+
!Float(value || 0).zero?
|
|
685
|
+
rescue ArgumentError, TypeError
|
|
686
|
+
false
|
|
194
687
|
end
|
|
195
688
|
|
|
196
689
|
def render_mesh_layer(canvas, layer, audio, color, index)
|
|
@@ -267,14 +760,115 @@ module Vizcore
|
|
|
267
760
|
end
|
|
268
761
|
|
|
269
762
|
def configured_color(params)
|
|
270
|
-
|
|
763
|
+
value = params[:color]
|
|
764
|
+
value = params["color"] unless value
|
|
765
|
+
resolved = resolve_color_value(value)
|
|
766
|
+
return resolved.to_s.strip unless resolved.to_s.strip.empty?
|
|
767
|
+
|
|
768
|
+
nil
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def resolve_color_value(value)
|
|
772
|
+
return value if value.is_a?(String)
|
|
773
|
+
return resolve_gradient_color(value) if value.is_a?(Hash)
|
|
774
|
+
|
|
775
|
+
value
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def resolve_gradient_color(value)
|
|
779
|
+
gradient = value[:gradient] || value["gradient"]
|
|
780
|
+
return nil unless gradient.is_a?(Hash)
|
|
781
|
+
|
|
782
|
+
colors = normalize_colors(gradient[:colors] || gradient["colors"])
|
|
783
|
+
return nil if colors.empty?
|
|
784
|
+
|
|
785
|
+
return colors[0] if colors.length == 1
|
|
786
|
+
|
|
787
|
+
position = normalize_position(gradient[:position] || gradient["position"])
|
|
788
|
+
stops = normalize_gradient_stops(gradient[:stops] || gradient["stops"], colors.length)
|
|
789
|
+
|
|
790
|
+
if stops
|
|
791
|
+
resolve_gradient_color_with_stops(colors, position, stops)
|
|
792
|
+
else
|
|
793
|
+
resolve_gradient_color_with_position(colors, position)
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def normalize_gradient_stops(stops, color_count)
|
|
798
|
+
return nil unless stops
|
|
799
|
+
|
|
800
|
+
values = Array(stops).filter_map { |entry| Float(entry, exception: false) }
|
|
801
|
+
return nil if values.length != color_count
|
|
802
|
+
|
|
803
|
+
values.sort.map { |value| value.to_f.clamp(0.0, 1.0) }
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def resolve_gradient_color_with_position(colors, position)
|
|
807
|
+
segment_length = 1.0 / (colors.length - 1)
|
|
808
|
+
segment = [(position / segment_length).floor, colors.length - 2].min
|
|
809
|
+
blend = (position % segment_length) / segment_length
|
|
810
|
+
|
|
811
|
+
left_color = parse_hex_color(colors[segment])
|
|
812
|
+
right_color = parse_hex_color(colors[segment + 1])
|
|
813
|
+
return colors[segment] if left_color.nil? || right_color.nil?
|
|
814
|
+
|
|
815
|
+
interpolate_hex_color(left_color, right_color, blend)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def resolve_gradient_color_with_stops(colors, position, stops)
|
|
819
|
+
index = Array.new(colors.length - 1) { |offset| offset }.index do |offset|
|
|
820
|
+
position <= stops[offset + 1]
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
return colors.last if index.nil?
|
|
824
|
+
|
|
825
|
+
return colors[0] if index == 0 && position <= stops[0]
|
|
826
|
+
|
|
827
|
+
left_index = [index, colors.length - 2].min
|
|
828
|
+
right_index = left_index + 1
|
|
829
|
+
start = stops[left_index]
|
|
830
|
+
stop = stops[right_index]
|
|
831
|
+
blend = ((position - start) / (stop - start)).clamp(0.0, 1.0)
|
|
832
|
+
|
|
833
|
+
left_color = parse_hex_color(colors[left_index])
|
|
834
|
+
right_color = parse_hex_color(colors[right_index])
|
|
835
|
+
return colors[left_index] if left_color.nil? || right_color.nil?
|
|
836
|
+
|
|
837
|
+
interpolate_hex_color(left_color, right_color, blend)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
def normalize_colors(value)
|
|
841
|
+
Array(value).map { |entry| entry.to_s.strip }.reject(&:empty?)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def normalize_position(value)
|
|
845
|
+
position = Float(value)
|
|
846
|
+
position = 0.0 unless position.finite?
|
|
847
|
+
|
|
848
|
+
position % 1.0
|
|
849
|
+
rescue ArgumentError, TypeError
|
|
850
|
+
0.0
|
|
271
851
|
end
|
|
272
852
|
|
|
273
853
|
def palette_color(params, index)
|
|
274
854
|
palette = Array(params[:palette] || params["palette"]).map { |color| color.to_s.strip }.reject(&:empty?)
|
|
275
855
|
return nil if palette.empty?
|
|
276
856
|
|
|
277
|
-
|
|
857
|
+
position = normalize_palette_position(index, palette.length)
|
|
858
|
+
return palette[0] if position.nil?
|
|
859
|
+
|
|
860
|
+
lower_index = position.floor
|
|
861
|
+
upper_index = (lower_index + 1) % palette.length
|
|
862
|
+
blend = position - lower_index
|
|
863
|
+
|
|
864
|
+
base_color = palette[lower_index]
|
|
865
|
+
return base_color unless blend.positive? && blend < 1.0
|
|
866
|
+
|
|
867
|
+
lower_rgb = parse_hex_color(base_color)
|
|
868
|
+
upper_rgb = parse_hex_color(palette[upper_index])
|
|
869
|
+
return base_color if lower_rgb.nil? || upper_rgb.nil?
|
|
870
|
+
|
|
871
|
+
interpolate_hex_color(lower_rgb, upper_rgb, blend)
|
|
278
872
|
end
|
|
279
873
|
|
|
280
874
|
def parse_hex_color(value)
|
|
@@ -286,6 +880,28 @@ module Vizcore
|
|
|
286
880
|
[hex[0, 2], hex[2, 2], hex[4, 2]].map { |component| component.to_i(16) }
|
|
287
881
|
end
|
|
288
882
|
|
|
883
|
+
def normalize_palette_position(value, palette_length)
|
|
884
|
+
return nil unless palette_length.positive?
|
|
885
|
+
|
|
886
|
+
numeric = Float(value)
|
|
887
|
+
return nil unless numeric.finite?
|
|
888
|
+
|
|
889
|
+
position = numeric % palette_length
|
|
890
|
+
return nil if position.nan?
|
|
891
|
+
|
|
892
|
+
position
|
|
893
|
+
rescue StandardError
|
|
894
|
+
nil
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def interpolate_hex_color(left_rgb, right_rgb, blend)
|
|
898
|
+
blend = blend.to_f.clamp(0.0, 1.0)
|
|
899
|
+
rgb = left_rgb.zip(right_rgb).map do |left, right|
|
|
900
|
+
(left + (right - left) * blend).round.clamp(0, 255)
|
|
901
|
+
end
|
|
902
|
+
format("##{rgb.map { |value| format('%02x', value) }.join}")
|
|
903
|
+
end
|
|
904
|
+
|
|
289
905
|
def default_layer
|
|
290
906
|
{ type: "geometry", name: "snapshot" }
|
|
291
907
|
end
|
|
@@ -316,11 +932,12 @@ module Vizcore
|
|
|
316
932
|
|
|
317
933
|
# Tiny RGBA canvas with alpha blending and a few primitive drawing helpers.
|
|
318
934
|
class Canvas
|
|
319
|
-
def initialize(width:, height:)
|
|
935
|
+
def initialize(width:, height:, transparent: false)
|
|
320
936
|
@width = width
|
|
321
937
|
@height = height
|
|
322
938
|
@bytes = String.new(capacity: width * height * 4, encoding: Encoding::BINARY)
|
|
323
|
-
|
|
939
|
+
alpha = transparent ? 0 : 255
|
|
940
|
+
@bytes << ([0, 0, 0, alpha].pack("C4") * (width * height))
|
|
324
941
|
end
|
|
325
942
|
|
|
326
943
|
attr_reader :width, :height, :bytes
|
|
@@ -426,7 +1043,8 @@ module Vizcore
|
|
|
426
1043
|
current = bytes.getbyte(offset + index)
|
|
427
1044
|
bytes.setbyte(offset + index, interpolate(current, color[index], amount).round)
|
|
428
1045
|
end
|
|
429
|
-
bytes.
|
|
1046
|
+
alpha = bytes.getbyte(offset + 3)
|
|
1047
|
+
bytes.setbyte(offset + 3, [alpha + (255 - alpha) * amount, 255].min.round)
|
|
430
1048
|
end
|
|
431
1049
|
|
|
432
1050
|
def set_pixel(x, y, color, alpha)
|