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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +70 -117
  3. data/docs/.nojekyll +0 -0
  4. data/docs/assets/playground-worker.js +373 -0
  5. data/docs/assets/playground.css +440 -0
  6. data/docs/assets/playground.js +652 -0
  7. data/docs/assets/site.css +744 -0
  8. data/docs/assets/vizcore-demo.gif +0 -0
  9. data/docs/assets/vizcore-poster.png +0 -0
  10. data/docs/assets/vj-tunnel.js +159 -0
  11. data/docs/index.html +225 -0
  12. data/docs/playground.html +81 -0
  13. data/docs/shape_dsl.md +269 -0
  14. data/examples/README.md +59 -0
  15. data/examples/assets/README.md +19 -0
  16. data/examples/audio_inspector.rb +34 -0
  17. data/examples/club_intro_drop.rb +78 -0
  18. data/examples/kansai_rubykaigi_visual.rb +70 -0
  19. data/examples/live_coding_minimal.rb +22 -0
  20. data/examples/midi_controller_show.rb +78 -0
  21. data/examples/midi_scene_switch.rb +3 -1
  22. data/examples/parser_visualizer.rb +48 -0
  23. data/examples/readme_demo.rb +17 -0
  24. data/examples/rhythm_geometry.rb +34 -0
  25. data/examples/ruby_crystal_show.rb +35 -0
  26. data/examples/shader_playground.rb +18 -0
  27. data/examples/unyo_liquid.rb +59 -0
  28. data/examples/vj_ambient_chill_room.rb +124 -0
  29. data/examples/vj_dnb_jungle.rb +170 -0
  30. data/examples/vj_festival_mainstage.rb +245 -0
  31. data/examples/vj_festival_mainstage.yml +17 -0
  32. data/examples/vj_glitch_industrial.rb +164 -0
  33. data/examples/vj_hiphop_cipher.rb +167 -0
  34. data/examples/vj_jpop_idol_live.rb +210 -0
  35. data/examples/vj_synthwave_retro.rb +173 -0
  36. data/examples/vj_techno_warehouse.rb +195 -0
  37. data/frontend/index.html +494 -2
  38. data/frontend/src/audio-inspector.js +40 -0
  39. data/frontend/src/custom-shape-param-controls.js +106 -0
  40. data/frontend/src/live-controls.js +131 -0
  41. data/frontend/src/main.js +1060 -16
  42. data/frontend/src/mapping-target-selector.js +109 -0
  43. data/frontend/src/midi-learn.js +194 -0
  44. data/frontend/src/performance-monitor.js +183 -0
  45. data/frontend/src/plugin-runtime.js +130 -0
  46. data/frontend/src/projector-mode.js +56 -0
  47. data/frontend/src/renderer/engine.js +157 -3
  48. data/frontend/src/renderer/layer-manager.js +442 -30
  49. data/frontend/src/renderer/shader-manager.js +26 -0
  50. data/frontend/src/runtime-control-preset.js +11 -0
  51. data/frontend/src/shader-error-overlay.js +29 -0
  52. data/frontend/src/shader-param-controls.js +93 -0
  53. data/frontend/src/shaders/builtins.js +380 -2
  54. data/frontend/src/shaders/post-effects.js +52 -0
  55. data/frontend/src/shape-editor-controls.js +157 -0
  56. data/frontend/src/visual-regression.js +67 -0
  57. data/frontend/src/visual-settings-preset.js +103 -0
  58. data/frontend/src/visuals/geometry.js +666 -0
  59. data/frontend/src/visuals/image-renderer.js +291 -0
  60. data/frontend/src/visuals/particle-system.js +56 -10
  61. data/frontend/src/visuals/shape-renderer.js +475 -0
  62. data/frontend/src/visuals/spectrogram-renderer.js +226 -0
  63. data/frontend/src/visuals/svg-arc.js +104 -0
  64. data/frontend/src/visuals/text-renderer.js +112 -11
  65. data/frontend/src/websocket-client.js +12 -1
  66. data/lib/vizcore/analysis/adaptive_normalizer.rb +70 -0
  67. data/lib/vizcore/analysis/beat_detector.rb +4 -2
  68. data/lib/vizcore/analysis/bpm_estimator.rb +8 -0
  69. data/lib/vizcore/analysis/feature_recorder.rb +159 -0
  70. data/lib/vizcore/analysis/feature_replay.rb +84 -0
  71. data/lib/vizcore/analysis/pipeline.rb +235 -11
  72. data/lib/vizcore/analysis/tap_tempo.rb +74 -0
  73. data/lib/vizcore/analysis.rb +4 -0
  74. data/lib/vizcore/audio/dummy_sine_input.rb +1 -1
  75. data/lib/vizcore/audio/fixture_input.rb +65 -0
  76. data/lib/vizcore/audio/input_manager.rb +4 -2
  77. data/lib/vizcore/audio/mic_input.rb +24 -8
  78. data/lib/vizcore/audio/portaudio_ffi.rb +106 -1
  79. data/lib/vizcore/audio.rb +1 -0
  80. data/lib/vizcore/cli/doctor.rb +159 -0
  81. data/lib/vizcore/cli/dsl_reference.rb +99 -0
  82. data/lib/vizcore/cli/layer_docs.rb +46 -0
  83. data/lib/vizcore/cli/scene_diagnostics.rb +23 -0
  84. data/lib/vizcore/cli/scene_inspector.rb +136 -0
  85. data/lib/vizcore/cli/scene_validator.rb +337 -0
  86. data/lib/vizcore/cli/shader_template.rb +68 -0
  87. data/lib/vizcore/cli/shader_uniform_docs.rb +54 -0
  88. data/lib/vizcore/cli.rb +689 -18
  89. data/lib/vizcore/config.rb +103 -2
  90. data/lib/vizcore/control_preset.rb +68 -0
  91. data/lib/vizcore/dsl/engine.rb +277 -5
  92. data/lib/vizcore/dsl/layer_builder.rb +1280 -23
  93. data/lib/vizcore/dsl/layer_group_builder.rb +112 -0
  94. data/lib/vizcore/dsl/mapping_resolver.rb +290 -7
  95. data/lib/vizcore/dsl/mapping_transform_builder.rb +71 -0
  96. data/lib/vizcore/dsl/reaction_builder.rb +44 -0
  97. data/lib/vizcore/dsl/scene_builder.rb +61 -5
  98. data/lib/vizcore/dsl/shader_source_resolver.rb +67 -6
  99. data/lib/vizcore/dsl/style_builder.rb +68 -0
  100. data/lib/vizcore/dsl/timeline_builder.rb +138 -0
  101. data/lib/vizcore/dsl/transition_controller.rb +77 -0
  102. data/lib/vizcore/dsl.rb +5 -1
  103. data/lib/vizcore/layer_catalog.rb +275 -0
  104. data/lib/vizcore/project_manifest.rb +152 -0
  105. data/lib/vizcore/renderer/png_writer.rb +57 -0
  106. data/lib/vizcore/renderer/render_sequence.rb +153 -0
  107. data/lib/vizcore/renderer/scene_frame_source.rb +132 -0
  108. data/lib/vizcore/renderer/scene_serializer.rb +36 -3
  109. data/lib/vizcore/renderer/snapshot.rb +38 -0
  110. data/lib/vizcore/renderer/snapshot_renderer.rb +938 -0
  111. data/lib/vizcore/renderer.rb +5 -0
  112. data/lib/vizcore/server/frame_broadcaster.rb +143 -8
  113. data/lib/vizcore/server/gallery_app.rb +155 -0
  114. data/lib/vizcore/server/gallery_page.rb +100 -0
  115. data/lib/vizcore/server/gallery_runner.rb +48 -0
  116. data/lib/vizcore/server/rack_app.rb +203 -4
  117. data/lib/vizcore/server/runner.rb +391 -22
  118. data/lib/vizcore/server/scene_dependency_watcher.rb +79 -0
  119. data/lib/vizcore/server/websocket_handler.rb +60 -10
  120. data/lib/vizcore/server.rb +4 -0
  121. data/lib/vizcore/shape.rb +719 -0
  122. data/lib/vizcore/sync/osc_message.rb +103 -0
  123. data/lib/vizcore/sync/osc_receiver.rb +68 -0
  124. data/lib/vizcore/sync.rb +4 -0
  125. data/lib/vizcore/templates/midi_control_scene.rb +3 -1
  126. data/lib/vizcore/templates/plugin_layer.rb +20 -0
  127. data/lib/vizcore/templates/plugin_readme.md +23 -0
  128. data/lib/vizcore/templates/plugin_renderer.js +43 -0
  129. data/lib/vizcore/templates/plugin_scene.rb +14 -0
  130. data/lib/vizcore/templates/project_readme.md +7 -23
  131. data/lib/vizcore/templates/rubykaigi_scene.rb +30 -0
  132. data/lib/vizcore/version.rb +1 -1
  133. data/lib/vizcore.rb +28 -0
  134. data/scripts/browser_capture.mjs +75 -0
  135. data/sig/vizcore.rbs +461 -0
  136. metadata +94 -3
  137. data/docs/GETTING_STARTED.md +0 -105
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 824691790c88752d418cb0e2ca49a39a15e76926cc8025ac51403b3ba2797f1c
4
- data.tar.gz: bc3ed3cd8282e51dbc21e175b6f8aa76ea7d9c7a7d1ccdfb6e2fc5dfa5b42374
3
+ metadata.gz: c750ff1ca41db0024074232a588262d5220e4063cdb057421c25465b1de102eb
4
+ data.tar.gz: 3123d34ed03e497dde1530bc41ade0c5177c519072713817429d5e8187d97059
5
5
  SHA512:
6
- metadata.gz: 4786e0f33292f0598c19c106d544f25775a721e76173bee09fd45f8917a8c148f10a30756c948753165937917f4e5e44c97295e12882243aba6605c07cd3746b
7
- data.tar.gz: 99cbff89caa54d6bbfedfd116754827eb3d09267d931839a4fae2dff65b3ae65ca5efeb8b43f5652c3f35e591aa4a50c37967f1942af66594ea5a3528167bb42
6
+ metadata.gz: ce9f65e426497957526ce5e73cd6eb4cfb0f5981fb1f3151cb12a39201d4d0bb3b5dad0b2563e5fe7220c2357e94e5efcb7956e49efd8167f836cbf89d22b875
7
+ data.tar.gz: b6ba9c3755a3ca4ba4e1fb4f163d55e0275c7ba513d8d39891893528238e39fe553cc020c14538bfac49e1a1f355e161e899904c1c0f57e77aa47e027444c774
data/README.md CHANGED
@@ -1,170 +1,123 @@
1
1
  # Vizcore [![Gem Version](https://badge.fury.io/rb/vizcore.svg)](https://badge.fury.io/rb/vizcore) [![CI](https://github.com/ydah/vizcore/actions/workflows/main.yml/badge.svg)](https://github.com/ydah/vizcore/actions/workflows/main.yml)
2
2
 
3
- Vizcore is a Ruby gem for building audio-reactive visuals with a Ruby DSL. Define scenes in pure Ruby, stream frames to the browser over WebSocket, and react to audio, beat, and MIDI in real time.
3
+ Vizcore is a Ruby gem for audio-reactive VJ visuals. Define scenes in Ruby, stream them to a browser renderer, and map audio analysis, beats, MIDI, OSC, and live controls to visual parameters.
4
4
 
5
- ## Installation
5
+ <p align="center">
6
+ <img src="docs/assets/vizcore-demo.gif" width="640" alt="Animated Vizcore demo where detected beats expand concentric rings" />
7
+ </p>
6
8
 
7
- ```bash
8
- gem install vizcore
9
- ```
9
+ ## Install
10
10
 
11
- Or add to your Gemfile:
11
+ Requirements:
12
12
 
13
- ```bash
14
- bundle add vizcore
15
- ```
16
-
17
- **System dependencies:**
13
+ - Ruby `>= 3.2`
14
+ - PortAudio for microphone input
15
+ - ffmpeg for MP3/FLAC input and MP4 output
16
+ - fftw3 optional for faster FFT analysis
18
17
 
19
- macOS:
20
18
  ```bash
21
- brew install portaudio ffmpeg # ffmpeg only needed for MP3/FLAC input
22
- brew install fftw # optional: faster FFT
19
+ # macOS
20
+ brew install portaudio ffmpeg
21
+ brew install fftw # optional
22
+
23
+ # Ubuntu / Debian
24
+ sudo apt install -y libportaudio2 libportaudio-dev ffmpeg
25
+ sudo apt install -y libfftw3-dev # optional
23
26
  ```
24
27
 
25
- Ubuntu/Debian:
26
28
  ```bash
27
- sudo apt install -y libportaudio2 libportaudio-dev ffmpeg
28
- sudo apt install -y libfftw3-dev # optional: faster FFT
29
+ gem install vizcore
30
+ # or
31
+ bundle add vizcore
29
32
  ```
30
33
 
31
34
  ## Quick Start
32
35
 
33
36
  ```bash
34
- vizcore start examples/basic.rb
37
+ vizcore doctor
38
+ vizcore demo
35
39
  ```
36
40
 
37
- Then open `http://127.0.0.1:4567`.
38
-
39
- For full setup, device listing, and troubleshooting, see [GETTING_STARTED.md](docs/GETTING_STARTED.md).
40
-
41
- ## Scene DSL
42
-
43
- Scenes are written in plain Ruby. Layers map audio analysis values to visual parameters:
44
-
45
- ```ruby
46
- Vizcore.define do
47
- scene :intro do
48
- layer :wireframe do
49
- type :wireframe_cube
50
- map amplitude => :rotation_speed
51
- map fft_spectrum => :deform
52
- map frequency_band(:high) => :color_shift
53
- end
54
- end
55
-
56
- scene :drop do
57
- layer :particles do
58
- type :particle_field
59
- count 3600
60
- map amplitude => :speed
61
- map frequency_band(:low) => :size
62
- end
41
+ Open `http://127.0.0.1:4567`.
63
42
 
64
- layer :title do
65
- type :text
66
- content "DROP"
67
- font_size 96
68
- map beat? => :flash
69
- end
70
- end
43
+ To run a scene file directly:
71
44
 
72
- transition from: :intro, to: :drop do
73
- trigger { beat_count >= 64 }
74
- effect :crossfade, duration: 1.4
75
- end
76
- end
45
+ ```bash
46
+ vizcore start examples/basic.rb
47
+ vizcore start examples/vj_techno_warehouse.rb --audio-source file --audio-file examples/assets/complex_demo_loop.wav
77
48
  ```
78
49
 
79
- ### Custom GLSL Shaders
80
-
81
- ```ruby
82
- layer :wave_shader do
83
- type :shader
84
- glsl "shaders/custom_wave.frag"
85
- map amplitude => :param_intensity
86
- map frequency_band(:low) => :param_bass
87
- map beat? => :param_flash
88
- end
89
- ```
50
+ ## Minimal Scene
90
51
 
91
- ### MIDI Scene Switching
52
+ Scenes are plain Ruby files:
92
53
 
93
54
  ```ruby
94
55
  Vizcore.define do
95
- midi :controller, device: :default
96
-
97
- scene :warmup do
98
- layer :grid do
99
- shader :neon_grid
100
- map frequency_band(:mid) => :intensity
56
+ scene :readme_demo do
57
+ layer :beat_rings do
58
+ palette "#24f6ff", "#ff2bbd", "#caff2e"
59
+
60
+ circle count: 4 do
61
+ radius 92
62
+ stroke 3
63
+ map beat_pulse,
64
+ to: :radius,
65
+ gain: 160.0,
66
+ min: 56,
67
+ max: 164,
68
+ attack: 1.0,
69
+ release: 0.2
70
+ end
101
71
  end
102
72
  end
103
-
104
- midi_map note: 36 do
105
- switch_scene :impact
106
- end
107
-
108
- midi_map cc: 1 do |value|
109
- set :global_intensity, value / 127.0
110
- end
111
73
  end
112
74
  ```
113
75
 
114
- ## CLI
76
+ Run it with:
115
77
 
116
78
  ```bash
117
- vizcore start SCENE_FILE [--host 127.0.0.1] [--port 4567] [--audio-source mic|file|dummy] [--audio-file PATH]
118
- vizcore new PROJECT_NAME
119
- vizcore devices [audio|midi]
79
+ vizcore start scene.rb
120
80
  ```
121
81
 
122
- ### Audio Sources
123
-
124
- | Source | Description |
125
- |--------|-------------|
126
- | `mic` | Live microphone input (default) |
127
- | `file` | File playback — `.wav` directly, `.mp3`/`.flac` via `ffmpeg` |
128
- | `dummy` | Silent source for layout testing |
82
+ ## Useful Commands
129
83
 
130
84
  ```bash
131
- # Microphone
132
- vizcore start scene.rb --audio-source mic
133
-
134
- # WAV file
135
- vizcore start scene.rb --audio-source file --audio-file track.wav
136
-
137
- # MP3/FLAC (requires ffmpeg)
138
- vizcore start scene.rb --audio-source file --audio-file set.mp3
85
+ vizcore start SCENE_FILE
86
+ vizcore start --manifest vizcore.yml
87
+ vizcore gallery
88
+ vizcore validate SCENE_FILE
89
+ vizcore devices audio
90
+ vizcore devices midi
91
+ vizcore snapshot SCENE_FILE --out screenshot.png
139
92
  ```
140
93
 
141
- When using file source, the HUD exposes **Play Audio** / **Pause Audio** controls and shows BPM, Beat, and Beat Count.
94
+ Use `vizcore help` for the full CLI.
142
95
 
143
- ## Requirements
96
+ Browser routes:
144
97
 
145
- - Ruby `>= 3.2`
146
- - `portaudio` for microphone input
147
- - `ffmpeg` on `PATH` when using `.mp3` or `.flac` file input
148
- - `fftw3` (optional) — Vizcore falls back to pure-Ruby FFT automatically when unavailable
98
+ - `/` visual output with operator controls
99
+ - `/projector` clean projection output
100
+ - `/control` separate operator panel
149
101
 
150
- ## Examples
102
+ ## Documentation
151
103
 
152
- | File | Description |
153
- |------|-------------|
154
- | `examples/basic.rb` | Single wireframe cube layer |
155
- | `examples/intro_drop.rb` | Beat-triggered scene transition |
156
- | `examples/file_audio_demo.rb` | File audio source walkthrough |
157
- | `examples/complex_audio_showcase.rb` | Dense multi-layer showcase |
158
- | `examples/midi_scene_switch.rb` | MIDI-driven scene switching |
159
- | `examples/custom_shader.rb` | Custom GLSL shader with audio mapping |
104
+ - Project site: <https://ydah.github.io/vizcore/>
105
+ - Examples: [examples/README.md](examples/README.md)
106
+ - Changelog: [CHANGELOG.md](CHANGELOG.md)
107
+ - Runtime layer reference: `vizcore layers`
108
+ - Ruby DSL reference: `vizcore dsl-docs`
109
+ - Shader uniform reference: `vizcore shader-docs`
160
110
 
161
111
  ## Development
162
112
 
163
113
  ```bash
114
+ bundle install
115
+ npm install --prefix frontend
164
116
  bundle exec rspec
117
+ npm --prefix frontend test
118
+ bundle exec rake release:verify
165
119
  ```
166
120
 
167
-
168
121
  ## License
169
122
 
170
123
  MIT
data/docs/.nojekyll ADDED
File without changes
@@ -0,0 +1,373 @@
1
+ import { DefaultRubyVM } from "https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.9.4/dist/browser/+esm";
2
+
3
+ const RUBY_WASM_URL = "https://cdn.jsdelivr.net/npm/@ruby/3.4-wasm-wasi@2.9.4/dist/ruby+stdlib.wasm";
4
+
5
+ const DSL_RUNTIME = `
6
+ require "base64"
7
+ require "json"
8
+ require "js"
9
+
10
+ module VizcorePlayground
11
+ class Source
12
+ attr_reader :kind, :name
13
+
14
+ def initialize(kind, name = nil)
15
+ @kind = kind.to_s
16
+ @name = name&.to_s
17
+ end
18
+
19
+ def to_h
20
+ output = { "source" => kind }
21
+ output["name"] = name if name
22
+ output
23
+ end
24
+ end
25
+
26
+ module Normalizer
27
+ module_function
28
+
29
+ def value(input)
30
+ case input
31
+ when Source
32
+ input.to_h
33
+ when Symbol
34
+ input.to_s
35
+ when Range
36
+ [input.begin, input.end]
37
+ when Array
38
+ input.map { |entry| value(entry) }
39
+ when Hash
40
+ input.each_with_object({}) { |(key, entry), output| output[key.to_s] = value(entry) }
41
+ else
42
+ input
43
+ end
44
+ end
45
+ end
46
+
47
+ module Sources
48
+ def amplitude = Source.new("amplitude")
49
+ def fft_spectrum = Source.new("fft_spectrum")
50
+ def beat? = Source.new("beat")
51
+ def beat = Source.new("beat")
52
+ def beat_pulse = Source.new("beat_pulse")
53
+ def beat_confidence = Source.new("beat_confidence")
54
+ def bass = Source.new("band", "low")
55
+ def low = Source.new("band", "low")
56
+ def mid = Source.new("band", "mid")
57
+ def treble = Source.new("band", "high")
58
+ def high = Source.new("band", "high")
59
+ def kick = Source.new("drum", "kick")
60
+ def snare = Source.new("drum", "snare")
61
+ def hihat = Source.new("drum", "hihat")
62
+
63
+ def frequency_band(name)
64
+ Source.new("band", name)
65
+ end
66
+
67
+ def onset(name = nil)
68
+ name ? Source.new("onset", name) : Source.new("onset")
69
+ end
70
+ end
71
+
72
+ class ShapeBuilder
73
+ include Sources
74
+
75
+ def initialize(type, attrs = {})
76
+ @shape = { "type" => type.to_s, "mappings" => [] }.merge(Normalizer.value(attrs))
77
+ end
78
+
79
+ def map(source = nil, target = nil, **options)
80
+ if source.is_a?(Hash)
81
+ source.each { |entry_source, entry_target| add_mapping(entry_source, entry_target, options) }
82
+ else
83
+ add_mapping(source, target || options.delete(:to), options)
84
+ end
85
+ end
86
+
87
+ def to_h = @shape
88
+
89
+ private
90
+
91
+ def add_mapping(source, target, options)
92
+ @shape["mappings"] << {
93
+ "source" => Normalizer.value(source),
94
+ "target" => target.to_s,
95
+ "transform" => Normalizer.value(options)
96
+ }
97
+ end
98
+
99
+ def method_missing(name, *args, &block)
100
+ return Source.new(name) if args.empty? && !block
101
+
102
+ @shape[name.to_s] = args.length <= 1 ? Normalizer.value(args.first) : Normalizer.value(args)
103
+ end
104
+
105
+ def respond_to_missing?(_name, _include_private = false) = true
106
+ end
107
+
108
+ class LayerBuilder
109
+ include Sources
110
+
111
+ attr_reader :name
112
+
113
+ def initialize(name)
114
+ @name = name.to_s
115
+ @type = "geometry"
116
+ @shader = nil
117
+ @params = {}
118
+ @mappings = []
119
+ @param_schema = []
120
+ end
121
+
122
+ def type(value)
123
+ @type = value.to_s
124
+ end
125
+
126
+ def shader(value, **options)
127
+ @type = "shader"
128
+ @shader = value.to_s
129
+ @params.merge!(Normalizer.value(options))
130
+ end
131
+
132
+ def glsl(path, **options)
133
+ @type = "shader"
134
+ @shader = "custom"
135
+ @params["glsl"] = path.to_s
136
+ @params.merge!(Normalizer.value(options))
137
+ end
138
+
139
+ def palette(*colors)
140
+ @params["palette"] = colors.map(&:to_s)
141
+ end
142
+
143
+ def blend(value)
144
+ @params["blend"] = value.to_s
145
+ end
146
+
147
+ def effect(value, **options)
148
+ @params["effect"] = value.to_s
149
+ @params["effect_options"] = Normalizer.value(options) unless options.empty?
150
+ end
151
+
152
+ def param(name, default:, range: nil, step: nil)
153
+ schema = { "name" => name.to_s, "default" => default }
154
+ if range
155
+ schema["min"] = range.begin
156
+ schema["max"] = range.end
157
+ end
158
+ schema["step"] = step if step
159
+ @param_schema << schema
160
+ @params["param_" + name.to_s] = default
161
+ end
162
+
163
+ def circle(count: 1, **attrs, &block)
164
+ shape = ShapeBuilder.new(:circle, { count: count }.merge(attrs))
165
+ shape.instance_eval(&block) if block
166
+ (@params["shapes"] ||= []) << shape.to_h
167
+ end
168
+
169
+ def line(**attrs)
170
+ (@params["shapes"] ||= []) << { "type" => "line" }.merge(Normalizer.value(attrs))
171
+ end
172
+
173
+ def map(source = nil, target = nil, **options)
174
+ if source.is_a?(Hash)
175
+ source.each { |entry_source, entry_target| add_mapping(entry_source, entry_target, options) }
176
+ else
177
+ add_mapping(source, target || options.delete(:to), options)
178
+ end
179
+ end
180
+
181
+ def to_h
182
+ output = {
183
+ "name" => name,
184
+ "type" => @type,
185
+ "params" => @params,
186
+ "mappings" => @mappings
187
+ }
188
+ output["shader"] = @shader if @shader
189
+ output["param_schema"] = @param_schema unless @param_schema.empty?
190
+ output
191
+ end
192
+
193
+ private
194
+
195
+ def add_mapping(source, target, options)
196
+ @mappings << {
197
+ "source" => Normalizer.value(source),
198
+ "target" => target.to_s,
199
+ "transform" => Normalizer.value(options)
200
+ }
201
+ end
202
+
203
+ def method_missing(name, *args, &block)
204
+ return Source.new(name) if args.empty? && !block
205
+
206
+ @params[name.to_s] = args.length <= 1 ? Normalizer.value(args.first) : Normalizer.value(args)
207
+ end
208
+
209
+ def respond_to_missing?(_name, _include_private = false) = true
210
+ end
211
+
212
+ class SceneBuilder
213
+ def initialize(name)
214
+ @name = name.to_s
215
+ @layers = []
216
+ end
217
+
218
+ def layer(name, &block)
219
+ builder = LayerBuilder.new(name)
220
+ builder.instance_eval(&block) if block
221
+ @layers << builder.to_h
222
+ end
223
+
224
+ def to_h
225
+ { "name" => @name, "layers" => @layers }
226
+ end
227
+ end
228
+
229
+ class TransitionBuilder
230
+ def initialize(from, to)
231
+ @transition = { "from" => from.to_s, "to" => to.to_s }
232
+ end
233
+
234
+ def on_bar(value)
235
+ @transition["on_bar"] = value
236
+ end
237
+
238
+ def effect(value, **options)
239
+ @transition["effect"] = value.to_s
240
+ @transition["duration"] = options[:duration] if options.key?(:duration)
241
+ end
242
+
243
+ def to_h = @transition
244
+ end
245
+
246
+ class DefinitionBuilder
247
+ include Sources
248
+
249
+ def initialize
250
+ @scenes = []
251
+ @transitions = []
252
+ @globals = {}
253
+ end
254
+
255
+ def scene(name, **_options, &block)
256
+ builder = SceneBuilder.new(name)
257
+ builder.instance_eval(&block) if block
258
+ @scenes << builder.to_h
259
+ end
260
+
261
+ def transition(from:, to:, &block)
262
+ builder = TransitionBuilder.new(from, to)
263
+ builder.instance_eval(&block) if block
264
+ @transitions << builder.to_h
265
+ end
266
+
267
+ def set(name, value)
268
+ @globals[name.to_s] = Normalizer.value(value)
269
+ end
270
+
271
+ def to_h
272
+ { "scenes" => @scenes, "transitions" => @transitions, "globals" => @globals }
273
+ end
274
+
275
+ def method_missing(_name, *_args, &_block)
276
+ nil
277
+ end
278
+
279
+ def respond_to_missing?(_name, _include_private = false) = true
280
+ end
281
+
282
+ class << self
283
+ attr_accessor :current_definition
284
+
285
+ def compile_and_post(id, encoded)
286
+ source = Base64.decode64(encoded).force_encoding("UTF-8")
287
+ self.current_definition = nil
288
+ definition = TOPLEVEL_BINDING.eval(source, "playground.rb", 1)
289
+ definition = current_definition unless definition.is_a?(Hash)
290
+ definition ||= { "scenes" => [], "transitions" => [], "globals" => {} }
291
+ JS.global.postMessage({
292
+ type: "compiled",
293
+ id: id,
294
+ definition_json: JSON.generate(definition)
295
+ }.to_js)
296
+ rescue Exception => error
297
+ JS.global.postMessage({
298
+ type: "error",
299
+ id: id,
300
+ message: error.message,
301
+ backtrace: Array(error.backtrace).first(8)
302
+ }.to_js)
303
+ end
304
+ end
305
+ end
306
+
307
+ module Vizcore
308
+ def self.define(&block)
309
+ builder = VizcorePlayground::DefinitionBuilder.new
310
+ builder.instance_eval(&block) if block
311
+ VizcorePlayground.current_definition = builder.to_h
312
+ end
313
+ end
314
+ `;
315
+
316
+ let vmPromise = null;
317
+
318
+ const postStatus = (message) => {
319
+ self.postMessage({ type: "status", message });
320
+ };
321
+
322
+ const compileWasm = async (response) => {
323
+ try {
324
+ return await WebAssembly.compileStreaming(response);
325
+ } catch (_error) {
326
+ const fallbackResponse = await fetch(RUBY_WASM_URL);
327
+ return WebAssembly.compile(await fallbackResponse.arrayBuffer());
328
+ }
329
+ };
330
+
331
+ const initializeVm = async () => {
332
+ postStatus("Loading Ruby wasm");
333
+ const response = await fetch(RUBY_WASM_URL);
334
+ const rubyModule = await compileWasm(response);
335
+ const { vm } = await DefaultRubyVM(rubyModule);
336
+ vm.eval(DSL_RUNTIME);
337
+ self.postMessage({ type: "ready" });
338
+ return vm;
339
+ };
340
+
341
+ const getVm = () => {
342
+ vmPromise ||= initializeVm();
343
+ return vmPromise;
344
+ };
345
+
346
+ const encodeBase64 = (source) => {
347
+ const bytes = new TextEncoder().encode(source);
348
+ let binary = "";
349
+ const chunkSize = 0x8000;
350
+ for (let index = 0; index < bytes.length; index += chunkSize) {
351
+ const chunk = bytes.subarray(index, index + chunkSize);
352
+ binary += String.fromCharCode(...chunk);
353
+ }
354
+ return btoa(binary);
355
+ };
356
+
357
+ self.addEventListener("message", async (event) => {
358
+ const message = event.data || {};
359
+ if (message.type !== "compile") return;
360
+
361
+ try {
362
+ const vm = await getVm();
363
+ postStatus("Evaluating Ruby DSL");
364
+ vm.eval('VizcorePlayground.compile_and_post(' + Number(message.id) + ', "' + encodeBase64(String(message.source || "")) + '")');
365
+ } catch (error) {
366
+ self.postMessage({
367
+ type: "error",
368
+ id: message.id,
369
+ message: error instanceof Error ? error.message : String(error),
370
+ backtrace: []
371
+ });
372
+ }
373
+ });