vizcore 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +26 -0
- data/frontend/src/custom-shape-param-controls.js +106 -0
- data/frontend/src/main.js +268 -0
- data/frontend/src/mapping-target-selector.js +109 -0
- data/frontend/src/renderer/engine.js +10 -1
- data/frontend/src/renderer/layer-manager.js +18 -4
- data/frontend/src/shape-editor-controls.js +157 -0
- data/frontend/src/visuals/geometry.js +425 -27
- data/frontend/src/visuals/shape-renderer.js +475 -0
- data/frontend/src/visuals/svg-arc.js +104 -0
- data/lib/vizcore/cli/dsl_reference.rb +1 -1
- data/lib/vizcore/cli/scene_validator.rb +92 -0
- data/lib/vizcore/dsl/layer_builder.rb +795 -7
- data/lib/vizcore/dsl/mapping_resolver.rb +158 -4
- data/lib/vizcore/layer_catalog.rb +4 -2
- data/lib/vizcore/renderer/scene_frame_source.rb +14 -1
- data/lib/vizcore/renderer/snapshot_renderer.rb +507 -15
- data/lib/vizcore/server/frame_broadcaster.rb +53 -4
- data/lib/vizcore/server/runner.rb +21 -0
- data/lib/vizcore/shape.rb +719 -0
- data/lib/vizcore/version.rb +1 -1
- data/lib/vizcore.rb +1 -0
- data/sig/vizcore.rbs +100 -1
- metadata +12 -1
|
@@ -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
|
+
});
|