vizcore 0.1.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 +70 -117
- data/docs/.nojekyll +0 -0
- data/docs/assets/playground-worker.js +373 -0
- data/docs/assets/playground.css +440 -0
- data/docs/assets/playground.js +652 -0
- data/docs/assets/site.css +744 -0
- data/docs/assets/vizcore-demo.gif +0 -0
- data/docs/assets/vizcore-poster.png +0 -0
- data/docs/assets/vj-tunnel.js +159 -0
- data/docs/index.html +225 -0
- data/docs/playground.html +81 -0
- data/docs/shape_dsl.md +269 -0
- data/examples/README.md +59 -0
- data/examples/assets/README.md +19 -0
- data/examples/audio_inspector.rb +34 -0
- data/examples/club_intro_drop.rb +78 -0
- data/examples/kansai_rubykaigi_visual.rb +70 -0
- data/examples/live_coding_minimal.rb +22 -0
- data/examples/midi_controller_show.rb +78 -0
- data/examples/midi_scene_switch.rb +3 -1
- data/examples/parser_visualizer.rb +48 -0
- data/examples/readme_demo.rb +17 -0
- data/examples/rhythm_geometry.rb +34 -0
- data/examples/ruby_crystal_show.rb +35 -0
- data/examples/shader_playground.rb +18 -0
- data/examples/unyo_liquid.rb +59 -0
- data/examples/vj_ambient_chill_room.rb +124 -0
- data/examples/vj_dnb_jungle.rb +170 -0
- data/examples/vj_festival_mainstage.rb +245 -0
- data/examples/vj_festival_mainstage.yml +17 -0
- data/examples/vj_glitch_industrial.rb +164 -0
- data/examples/vj_hiphop_cipher.rb +167 -0
- data/examples/vj_jpop_idol_live.rb +210 -0
- data/examples/vj_synthwave_retro.rb +173 -0
- data/examples/vj_techno_warehouse.rb +195 -0
- data/frontend/index.html +494 -2
- data/frontend/src/audio-inspector.js +40 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/live-controls.js +131 -0
- data/frontend/src/main.js +1060 -16
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/midi-learn.js +194 -0
- data/frontend/src/performance-monitor.js +183 -0
- data/frontend/src/plugin-runtime.js +130 -0
- data/frontend/src/projector-mode.js +56 -0
- data/frontend/src/renderer/engine.js +157 -3
- data/frontend/src/renderer/layer-manager.js +442 -30
- data/frontend/src/renderer/shader-manager.js +26 -0
- data/frontend/src/runtime-control-preset.js +11 -0
- data/frontend/src/shader-error-overlay.js +29 -0
- data/frontend/src/shader-param-controls.js +93 -0
- data/frontend/src/shaders/builtins.js +380 -2
- data/frontend/src/shaders/post-effects.js +52 -0
- data/frontend/src/shape-editor-controls.js +157 -0
- data/frontend/src/visual-regression.js +67 -0
- data/frontend/src/visual-settings-preset.js +103 -0
- data/frontend/src/visuals/geometry.js +666 -0
- data/frontend/src/visuals/image-renderer.js +291 -0
- data/frontend/src/visuals/particle-system.js +56 -10
- data/frontend/src/visuals/shape-renderer.js +475 -0
- data/frontend/src/visuals/spectrogram-renderer.js +226 -0
- data/frontend/src/visuals/svg-arc.js +104 -0
- data/frontend/src/visuals/text-renderer.js +112 -11
- data/frontend/src/websocket-client.js +12 -1
- data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
- data/lib/vizcore/analysis/beat_detector.rb +4 -2
- data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
- data/lib/vizcore/analysis/feature_recorder.rb +159 -0
- data/lib/vizcore/analysis/feature_replay.rb +84 -0
- data/lib/vizcore/analysis/pipeline.rb +235 -11
- data/lib/vizcore/analysis/tap_tempo.rb +74 -0
- data/lib/vizcore/analysis.rb +4 -0
- data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
- data/lib/vizcore/audio/fixture_input.rb +65 -0
- data/lib/vizcore/audio/input_manager.rb +4 -2
- data/lib/vizcore/audio/mic_input.rb +24 -8
- data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
- data/lib/vizcore/audio.rb +1 -0
- data/lib/vizcore/cli/doctor.rb +159 -0
- data/lib/vizcore/cli/dsl_reference.rb +99 -0
- data/lib/vizcore/cli/layer_docs.rb +46 -0
- data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
- data/lib/vizcore/cli/scene_inspector.rb +136 -0
- data/lib/vizcore/cli/scene_validator.rb +337 -0
- data/lib/vizcore/cli/shader_template.rb +68 -0
- data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
- data/lib/vizcore/cli.rb +689 -18
- data/lib/vizcore/config.rb +103 -2
- data/lib/vizcore/control_preset.rb +68 -0
- data/lib/vizcore/dsl/engine.rb +277 -5
- data/lib/vizcore/dsl/layer_builder.rb +1280 -23
- data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
- data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
- data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
- data/lib/vizcore/dsl/reaction_builder.rb +44 -0
- data/lib/vizcore/dsl/scene_builder.rb +61 -5
- data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
- data/lib/vizcore/dsl/style_builder.rb +68 -0
- data/lib/vizcore/dsl/timeline_builder.rb +138 -0
- data/lib/vizcore/dsl/transition_controller.rb +77 -0
- data/lib/vizcore/dsl.rb +5 -1
- data/lib/vizcore/layer_catalog.rb +275 -0
- data/lib/vizcore/project_manifest.rb +152 -0
- data/lib/vizcore/renderer/png_writer.rb +57 -0
- data/lib/vizcore/renderer/render_sequence.rb +153 -0
- data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
- data/lib/vizcore/renderer/scene_serializer.rb +36 -3
- data/lib/vizcore/renderer/snapshot.rb +38 -0
- data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
- data/lib/vizcore/renderer.rb +5 -0
- data/lib/vizcore/server/frame_broadcaster.rb +143 -8
- data/lib/vizcore/server/gallery_app.rb +155 -0
- data/lib/vizcore/server/gallery_page.rb +100 -0
- data/lib/vizcore/server/gallery_runner.rb +48 -0
- data/lib/vizcore/server/rack_app.rb +203 -4
- data/lib/vizcore/server/runner.rb +391 -22
- data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
- data/lib/vizcore/server/websocket_handler.rb +60 -10
- data/lib/vizcore/server.rb +4 -0
- data/lib/vizcore/shape.rb +719 -0
- data/lib/vizcore/sync/osc_message.rb +103 -0
- data/lib/vizcore/sync/osc_receiver.rb +68 -0
- data/lib/vizcore/sync.rb +4 -0
- data/lib/vizcore/templates/midi_control_scene.rb +3 -1
- data/lib/vizcore/templates/plugin_layer.rb +20 -0
- data/lib/vizcore/templates/plugin_readme.md +23 -0
- data/lib/vizcore/templates/plugin_renderer.js +43 -0
- data/lib/vizcore/templates/plugin_scene.rb +14 -0
- data/lib/vizcore/templates/project_readme.md +7 -23
- data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +28 -0
- data/scripts/browser_capture.mjs +75 -0
- data/sig/vizcore.rbs +461 -0
- metadata +94 -3
- data/docs/GETTING_STARTED.md +0 -105
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vizcore
|
|
4
|
+
module Sync
|
|
5
|
+
# Minimal OSC 1.0 message parser for control sync.
|
|
6
|
+
class OscMessage
|
|
7
|
+
attr_reader :address, :arguments
|
|
8
|
+
|
|
9
|
+
# @param data [String]
|
|
10
|
+
# @return [Vizcore::Sync::OscMessage, nil]
|
|
11
|
+
def self.parse(data)
|
|
12
|
+
parser = Parser.new(data)
|
|
13
|
+
address = parser.read_string
|
|
14
|
+
return nil unless address&.start_with?("/")
|
|
15
|
+
|
|
16
|
+
tags = parser.read_string
|
|
17
|
+
arguments = parser.read_arguments(tags)
|
|
18
|
+
new(address: address, arguments: arguments)
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param address [String]
|
|
24
|
+
# @param arguments [Array]
|
|
25
|
+
def initialize(address:, arguments: [])
|
|
26
|
+
@address = address
|
|
27
|
+
@arguments = Array(arguments)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @api private
|
|
31
|
+
class Parser
|
|
32
|
+
# @param data [String]
|
|
33
|
+
def initialize(data)
|
|
34
|
+
@data = data.to_s.b
|
|
35
|
+
@offset = 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [String, nil]
|
|
39
|
+
def read_string
|
|
40
|
+
start = @offset
|
|
41
|
+
@offset += 1 while @offset < @data.bytesize && @data.getbyte(@offset) != 0
|
|
42
|
+
return nil if @offset >= @data.bytesize
|
|
43
|
+
|
|
44
|
+
value = @data.byteslice(start...@offset).to_s.force_encoding(Encoding::UTF_8)
|
|
45
|
+
@offset += 1
|
|
46
|
+
align_offset
|
|
47
|
+
value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param tags [String, nil]
|
|
51
|
+
# @return [Array]
|
|
52
|
+
def read_arguments(tags)
|
|
53
|
+
return [] if tags.to_s.empty?
|
|
54
|
+
return [] unless tags.start_with?(",")
|
|
55
|
+
|
|
56
|
+
tags[1..].to_s.each_char.map do |tag|
|
|
57
|
+
read_argument(tag)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def read_argument(tag)
|
|
64
|
+
case tag
|
|
65
|
+
when "i"
|
|
66
|
+
read_int32
|
|
67
|
+
when "f"
|
|
68
|
+
read_float32
|
|
69
|
+
when "s"
|
|
70
|
+
read_string
|
|
71
|
+
when "T"
|
|
72
|
+
true
|
|
73
|
+
when "F"
|
|
74
|
+
false
|
|
75
|
+
else
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def read_int32
|
|
81
|
+
value = read_bytes(4).unpack1("N")
|
|
82
|
+
value >= 0x80000000 ? value - 0x100000000 : value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def read_float32
|
|
86
|
+
read_bytes(4).unpack1("g")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def read_bytes(length)
|
|
90
|
+
raise ArgumentError, "OSC payload truncated" if @offset + length > @data.bytesize
|
|
91
|
+
|
|
92
|
+
@data.byteslice(@offset, length).tap do
|
|
93
|
+
@offset += length
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def align_offset
|
|
98
|
+
@offset += 1 while (@offset % 4).positive?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require_relative "osc_message"
|
|
5
|
+
|
|
6
|
+
module Vizcore
|
|
7
|
+
module Sync
|
|
8
|
+
# Receives OSC UDP messages on a background thread.
|
|
9
|
+
class OscReceiver
|
|
10
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
11
|
+
MAX_PACKET_SIZE = 65_536
|
|
12
|
+
|
|
13
|
+
# @param port [Integer]
|
|
14
|
+
# @param host [String]
|
|
15
|
+
# @param handler [#call]
|
|
16
|
+
# @param error_reporter [#call, nil]
|
|
17
|
+
def initialize(port:, host: DEFAULT_HOST, handler:, error_reporter: nil)
|
|
18
|
+
@host = host.to_s
|
|
19
|
+
@port = Integer(port)
|
|
20
|
+
@handler = handler
|
|
21
|
+
@error_reporter = error_reporter
|
|
22
|
+
@socket = nil
|
|
23
|
+
@thread = nil
|
|
24
|
+
@running = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Vizcore::Sync::OscReceiver]
|
|
28
|
+
def start
|
|
29
|
+
return self if @thread&.alive?
|
|
30
|
+
|
|
31
|
+
@socket = UDPSocket.new
|
|
32
|
+
@socket.bind(@host, @port)
|
|
33
|
+
@running = true
|
|
34
|
+
@thread = Thread.new { receive_loop }
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [nil]
|
|
39
|
+
def stop
|
|
40
|
+
@running = false
|
|
41
|
+
@socket&.close
|
|
42
|
+
@thread&.join(1)
|
|
43
|
+
nil
|
|
44
|
+
rescue StandardError
|
|
45
|
+
nil
|
|
46
|
+
ensure
|
|
47
|
+
@socket = nil
|
|
48
|
+
@thread = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def receive_loop
|
|
54
|
+
while @running
|
|
55
|
+
begin
|
|
56
|
+
data, = @socket.recvfrom(MAX_PACKET_SIZE)
|
|
57
|
+
message = OscMessage.parse(data)
|
|
58
|
+
@handler.call(message) if message
|
|
59
|
+
rescue IOError, SystemCallError
|
|
60
|
+
break unless @running
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
@error_reporter&.call("OSC message ignored: #{e.message}")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/vizcore/sync.rb
ADDED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# {{project_name}} MIDI mapping example.
|
|
4
4
|
Vizcore.define do
|
|
5
|
+
set :global_intensity, 0.65
|
|
6
|
+
|
|
5
7
|
midi :controller, device: :default
|
|
6
8
|
|
|
7
9
|
scene :warmup do
|
|
8
10
|
layer :warm_bg do
|
|
9
|
-
shader :
|
|
11
|
+
shader :waveform_ribbon
|
|
10
12
|
map frequency_band(:mid) => :intensity
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "vizcore"
|
|
4
|
+
|
|
5
|
+
module {{plugin_module}}
|
|
6
|
+
LAYER_TYPE = :{{plugin_type}}
|
|
7
|
+
|
|
8
|
+
Vizcore.register_layer_capability(
|
|
9
|
+
type: LAYER_TYPE,
|
|
10
|
+
aliases: [:{{plugin_name}}],
|
|
11
|
+
params: {
|
|
12
|
+
intensity: "Float",
|
|
13
|
+
color: "CSS color",
|
|
14
|
+
blend: "Blend mode",
|
|
15
|
+
opacity: "Float"
|
|
16
|
+
},
|
|
17
|
+
mappable_params: %i[intensity opacity],
|
|
18
|
+
description: "{{plugin_title}} browser-rendered plugin layer."
|
|
19
|
+
)
|
|
20
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# {{plugin_title}}
|
|
2
|
+
|
|
3
|
+
Vizcore plugin scaffold generated by `vizcore plugin new`.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `lib/{{plugin_name}}.rb` registers the Ruby layer capability used by validation and docs.
|
|
8
|
+
- `frontend/{{plugin_name}}-renderer.js` registers a browser renderer through `globalThis.VizcorePlugins`.
|
|
9
|
+
- `examples/{{plugin_name}}_scene.rb` shows the layer type in a scene.
|
|
10
|
+
|
|
11
|
+
## Try It
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require_relative "lib/{{plugin_name}}"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Serve `frontend/{{plugin_name}}-renderer.js` with your Vizcore frontend and load
|
|
18
|
+
it after the built-in app script. The browser runtime exposes
|
|
19
|
+
`globalThis.VizcorePlugins.apiVersion`; this scaffold expects version 1 or
|
|
20
|
+
newer. The browser renderer returns line points, so Vizcore can composite it
|
|
21
|
+
with normal layer `blend` and `opacity` params. Shader plugins can instead
|
|
22
|
+
register with `registerShaderRenderer(type, renderer)` and return a GLSL
|
|
23
|
+
fragment shader string or `{ kind: "shader", fragmentShader }`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const layerType = "{{plugin_type}}";
|
|
2
|
+
|
|
3
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
|
|
4
|
+
|
|
5
|
+
export const {{plugin_renderer}} = ({ audio, time, layer }) => {
|
|
6
|
+
const params = layer?.params || {};
|
|
7
|
+
const amplitude = clamp(Number(audio?.amplitude || 0), 0, 1);
|
|
8
|
+
const intensity = clamp(Number(params.intensity ?? 1), 0, 4);
|
|
9
|
+
const radius = 0.25 + amplitude * intensity * 0.18;
|
|
10
|
+
const phase = Number(time || 0) * 0.8;
|
|
11
|
+
const points = [];
|
|
12
|
+
|
|
13
|
+
for (let index = 0; index < 18; index += 1) {
|
|
14
|
+
const angle = phase + index * Math.PI / 9;
|
|
15
|
+
const nextAngle = angle + Math.PI * 0.35;
|
|
16
|
+
points.push(
|
|
17
|
+
Math.cos(angle) * radius,
|
|
18
|
+
Math.sin(angle) * radius,
|
|
19
|
+
Math.cos(nextAngle) * (radius + 0.18),
|
|
20
|
+
Math.sin(nextAngle) * (radius + 0.18),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
kind: "lines",
|
|
26
|
+
color: [0.45 + amplitude * 0.35, 0.9, 1.0],
|
|
27
|
+
points,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const register = () => {
|
|
32
|
+
const runtime = globalThis.VizcorePlugins;
|
|
33
|
+
if (!runtime?.registerLayerRenderer || Number(runtime.apiVersion || 0) < 1) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
runtime.registerLayerRenderer(layerType, {{plugin_renderer}});
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (!register() && typeof globalThis.addEventListener === "function") {
|
|
42
|
+
globalThis.addEventListener("vizcore:plugins-ready", register, { once: true });
|
|
43
|
+
}
|
|
@@ -2,34 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
This project was generated by `vizcore new`.
|
|
4
4
|
|
|
5
|
+
Template: `{{template_name}}`
|
|
6
|
+
|
|
5
7
|
## Start
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
vizcore start
|
|
10
|
+
vizcore start {{start_scene}}
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
## Included
|
|
12
|
-
|
|
13
|
-
- `scenes/basic.rb`: Minimal wireframe starter
|
|
14
|
-
- `scenes/intro_drop.rb`: Transition flow with beat trigger
|
|
15
|
-
- `scenes/midi_control.rb`: MIDI note/CC mapping example
|
|
16
|
-
- `scenes/custom_shader.rb`: Custom GLSL + post/VJ effect example
|
|
17
|
-
|
|
18
|
-
## Custom Shader
|
|
19
|
-
|
|
20
|
-
`scenes/custom_shader.rb` references `shaders/custom_wave.frag`.
|
|
21
|
-
Edit that file and save to see hot-reload updates.
|
|
22
|
-
|
|
23
|
-
## MIDI
|
|
13
|
+
## Included Files
|
|
24
14
|
|
|
25
|
-
|
|
15
|
+
{{included_files}}
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
vizcore devices midi
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Start MIDI example:
|
|
17
|
+
## Notes
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
vizcore start scenes/midi_control.rb
|
|
35
|
-
```
|
|
19
|
+
{{template_notes}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# {{project_name}} Ruby conference visual starter.
|
|
4
|
+
Vizcore.define do
|
|
5
|
+
scene :rubykaigi do
|
|
6
|
+
layer :ruby_grid do
|
|
7
|
+
shader :neon_grid
|
|
8
|
+
opacity 0.72
|
|
9
|
+
blend :screen
|
|
10
|
+
map frequency_band(:mid) => :intensity
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
layer :ruby_pulse do
|
|
14
|
+
type :wireframe_cube
|
|
15
|
+
blend :add
|
|
16
|
+
map amplitude => :rotation_speed, range: 0.5..3.2
|
|
17
|
+
map fft_spectrum => :deform
|
|
18
|
+
map frequency_band(:high) => :color_shift
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
layer :title do
|
|
22
|
+
type :text
|
|
23
|
+
content "{{project_name}}"
|
|
24
|
+
font_size 88
|
|
25
|
+
color "#e11d48"
|
|
26
|
+
glow_strength 0.35
|
|
27
|
+
map beat? => :flash
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/vizcore/version.rb
CHANGED
data/lib/vizcore.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "vizcore/version"
|
|
4
4
|
require_relative "vizcore/errors"
|
|
5
|
+
require_relative "vizcore/layer_catalog"
|
|
6
|
+
require_relative "vizcore/shape"
|
|
5
7
|
require_relative "vizcore/dsl"
|
|
6
8
|
require "pathname"
|
|
7
9
|
|
|
@@ -34,4 +36,30 @@ module Vizcore
|
|
|
34
36
|
def self.define(&block)
|
|
35
37
|
DSL::Engine.define(&block)
|
|
36
38
|
end
|
|
39
|
+
|
|
40
|
+
# Load a Vizcore plugin by Ruby require path.
|
|
41
|
+
#
|
|
42
|
+
# @param name [String, Symbol] require path for the plugin
|
|
43
|
+
# @return [true]
|
|
44
|
+
def self.plugin(name)
|
|
45
|
+
require name.to_s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Register a plugin-provided layer capability for validation and docs.
|
|
49
|
+
#
|
|
50
|
+
# @param type [Symbol, String] primary layer type
|
|
51
|
+
# @param aliases [Array<Symbol, String>] supported aliases
|
|
52
|
+
# @param params [Hash] layer parameter metadata
|
|
53
|
+
# @param mappable_params [Array<Symbol, String>] params that can be mapped
|
|
54
|
+
# @param description [String, nil] human-readable docs text
|
|
55
|
+
# @return [Vizcore::LayerCatalog::Capability]
|
|
56
|
+
def self.register_layer_capability(type:, aliases: [], params: {}, mappable_params: [], description: nil)
|
|
57
|
+
LayerCatalog.register_layer_capability(
|
|
58
|
+
type: type,
|
|
59
|
+
aliases: aliases,
|
|
60
|
+
params: params,
|
|
61
|
+
mappable_params: mappable_params,
|
|
62
|
+
description: description
|
|
63
|
+
)
|
|
64
|
+
end
|
|
37
65
|
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const options = parseArgs(process.argv.slice(2));
|
|
5
|
+
|
|
6
|
+
if (!options.url) {
|
|
7
|
+
console.error("Usage: node scripts/browser_capture.mjs URL --out browser-capture.png [--selector #vizcore-canvas] [--wait 1000]");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let chromium;
|
|
12
|
+
try {
|
|
13
|
+
({ chromium } = await import("playwright"));
|
|
14
|
+
} catch {
|
|
15
|
+
console.error("Playwright is required for browser capture. Install it with `npm install -D playwright` in the frontend project.");
|
|
16
|
+
process.exit(2);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const browser = await chromium.launch({ headless: true });
|
|
20
|
+
try {
|
|
21
|
+
const page = await browser.newPage({ viewport: { width: options.width, height: options.height } });
|
|
22
|
+
await page.goto(options.url, { waitUntil: "domcontentloaded" });
|
|
23
|
+
if (options.wait > 0) {
|
|
24
|
+
await page.waitForTimeout(options.wait);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const element = page.locator(options.selector).first();
|
|
28
|
+
await element.waitFor({ state: "visible", timeout: 10000 });
|
|
29
|
+
await fs.mkdir(path.dirname(options.out), { recursive: true });
|
|
30
|
+
await element.screenshot({ path: options.out });
|
|
31
|
+
console.log(`Browser capture written: ${options.out}`);
|
|
32
|
+
} finally {
|
|
33
|
+
await browser.close();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArgs(args) {
|
|
37
|
+
const parsed = {
|
|
38
|
+
url: null,
|
|
39
|
+
out: "browser-capture.png",
|
|
40
|
+
selector: "#vizcore-canvas",
|
|
41
|
+
wait: 1000,
|
|
42
|
+
width: 1280,
|
|
43
|
+
height: 720,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
47
|
+
const value = args[index];
|
|
48
|
+
if (value === "--out") {
|
|
49
|
+
parsed.out = String(args[index + 1] || parsed.out);
|
|
50
|
+
index += 1;
|
|
51
|
+
} else if (value === "--selector") {
|
|
52
|
+
parsed.selector = String(args[index + 1] || parsed.selector);
|
|
53
|
+
index += 1;
|
|
54
|
+
} else if (value === "--wait") {
|
|
55
|
+
parsed.wait = finiteNumber(args[index + 1], parsed.wait);
|
|
56
|
+
index += 1;
|
|
57
|
+
} else if (value === "--width") {
|
|
58
|
+
parsed.width = finiteNumber(args[index + 1], parsed.width);
|
|
59
|
+
index += 1;
|
|
60
|
+
} else if (value === "--height") {
|
|
61
|
+
parsed.height = finiteNumber(args[index + 1], parsed.height);
|
|
62
|
+
index += 1;
|
|
63
|
+
} else if (!parsed.url) {
|
|
64
|
+
parsed.url = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
parsed.out = path.resolve(parsed.out);
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function finiteNumber(value, fallback) {
|
|
73
|
+
const numeric = Number(value);
|
|
74
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
75
|
+
}
|