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.
@@ -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
+ });