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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c750ff1ca41db0024074232a588262d5220e4063cdb057421c25465b1de102eb
|
|
4
|
+
data.tar.gz: 3123d34ed03e497dde1530bc41ade0c5177c519072713817429d5e8187d97059
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce9f65e426497957526ce5e73cd6eb4cfb0f5981fb1f3151cb12a39201d4d0bb3b5dad0b2563e5fe7220c2357e94e5efcb7956e49efd8167f836cbf89d22b875
|
|
7
|
+
data.tar.gz: b6ba9c3755a3ca4ba4e1fb4f163d55e0275c7ba513d8d39891893528238e39fe553cc020c14538bfac49e1a1f355e161e899904c1c0f57e77aa47e027444c774
|
data/README.md
CHANGED
|
@@ -1,170 +1,123 @@
|
|
|
1
1
|
# Vizcore [](https://badge.fury.io/rb/vizcore) [](https://github.com/ydah/vizcore/actions/workflows/main.yml)
|
|
2
2
|
|
|
3
|
-
Vizcore is a Ruby gem for
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
gem install vizcore
|
|
9
|
-
```
|
|
9
|
+
## Install
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Requirements:
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
22
|
-
brew install
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
37
|
+
vizcore doctor
|
|
38
|
+
vizcore demo
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
+
Scenes are plain Ruby files:
|
|
92
53
|
|
|
93
54
|
```ruby
|
|
94
55
|
Vizcore.define do
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
76
|
+
Run it with:
|
|
115
77
|
|
|
116
78
|
```bash
|
|
117
|
-
vizcore start
|
|
118
|
-
vizcore new PROJECT_NAME
|
|
119
|
-
vizcore devices [audio|midi]
|
|
79
|
+
vizcore start scene.rb
|
|
120
80
|
```
|
|
121
81
|
|
|
122
|
-
|
|
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
|
-
|
|
132
|
-
vizcore start
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
vizcore
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
94
|
+
Use `vizcore help` for the full CLI.
|
|
142
95
|
|
|
143
|
-
|
|
96
|
+
Browser routes:
|
|
144
97
|
|
|
145
|
-
-
|
|
146
|
-
- `
|
|
147
|
-
- `
|
|
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
|
-
##
|
|
102
|
+
## Documentation
|
|
151
103
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
+
});
|