webvas 0.1.0 → 0.3.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.
data/js/loader.js CHANGED
@@ -6,6 +6,7 @@
6
6
  const runtimeUrl = loader.dataset.runtime || new URL("./assets/webvas.wasm", base).href;
7
7
  const workerUrl = new URL("./worker.js", base).href;
8
8
  const bridgeUrl = new URL("./bridge.js", base).href;
9
+ const shaderUrl = new URL("./wgsl-runner.js", base).href;
9
10
 
10
11
  async function run(sourceElement) {
11
12
  const selector = sourceElement.dataset.canvas || "#screen";
@@ -19,22 +20,35 @@
19
20
  canvas.insertAdjacentElement("afterend", status);
20
21
  const report = message => { status.textContent = message; };
21
22
  report("Loading Ruby runtime…");
23
+
24
+ const gpu = document.querySelector("#gpu") || document.createElement("canvas");
25
+ gpu.hidden = true;
26
+ if (!gpu.isConnected) {
27
+ gpu.id = "gpu";
28
+ gpu.hidden = true;
29
+ canvas.insertAdjacentElement("afterend", gpu);
30
+ }
22
31
  if (canvas.tabIndex < 0) canvas.tabIndex = 0;
23
32
 
24
33
  const response = await fetch(workerUrl, { credentials: "omit" });
25
34
  if (!response.ok) throw new Error(`Worker download failed: ${response.status}`);
26
35
  const workerBlobUrl = URL.createObjectURL(new Blob([await response.text()], { type: "text/javascript" }));
27
36
  const worker = new Worker(workerBlobUrl, { type: "module", name: "webvas" });
37
+ const screenSelector = selector;
28
38
  worker.addEventListener("message", event => {
29
39
  const message = event.data || {};
30
40
  if (message.type === "webvas:loading") report(message.message || "Loading Ruby runtime…");
31
41
  else if (message.type === "webvas:ready") {
32
42
  URL.revokeObjectURL(workerBlobUrl);
33
43
  worker.postMessage({ type: "webvas:run", source: sourceElement.textContent });
34
- } else if (message.type === "webvas:started") report("Ruby sketch is running");
44
+ }
45
+ else if (message.type === "webvas:started") report("Ruby sketch is running");
35
46
  else if (message.type === "webvas:error") report([message.message, message.backtrace].filter(Boolean).join("\n"));
36
- else if (message.type === "webvas:pixelated" && message.selector === selector) {
47
+ else if (message.type === "webvas:pixelated" && message.selector === screenSelector) {
37
48
  canvas.style.imageRendering = message.enabled ? "pixelated" : "auto";
49
+ } else if (message.type === "webvas:mode") {
50
+ canvas.hidden = message.canvas !== screenSelector;
51
+ gpu.hidden = message.canvas !== "#gpu";
38
52
  }
39
53
  });
40
54
  worker.addEventListener("error", event => {
@@ -44,12 +58,11 @@
44
58
 
45
59
  const forward = event => {
46
60
  if (event.type === "wheel") event.preventDefault();
47
- if (event.type === "keydown" && event.code === "Space") event.preventDefault();
48
61
  if (event.type === "pointerdown") {
49
62
  canvas.focus({ preventScroll: true });
50
63
  canvas.setPointerCapture(event.pointerId);
51
64
  }
52
- const rect = canvas.getBoundingClientRect();
65
+ const rect = event.currentTarget.getBoundingClientRect();
53
66
  worker.postMessage({
54
67
  type: "webvas:input",
55
68
  event: {
@@ -66,11 +79,12 @@
66
79
  canvas.addEventListener(type, forward, type === "wheel" ? { passive: false } : undefined);
67
80
  }
68
81
 
69
- const offscreen = canvas.transferControlToOffscreen();
82
+ const screen = canvas.transferControlToOffscreen();
83
+ const gpuCanvas = gpu.transferControlToOffscreen();
70
84
  worker.postMessage({
71
- type: "webvas:init", canvas: offscreen, selector,
72
- bridgeUrl, wasmUrl: new URL(runtimeUrl, document.baseURI).href
73
- }, [offscreen]);
85
+ type: "webvas:init", screen, gpu: gpuCanvas, screenSelector, gpuSelector: "#gpu",
86
+ bridgeUrl, shaderUrl, wasmUrl: new URL(runtimeUrl, document.baseURI).href
87
+ }, [screen, gpuCanvas]);
74
88
  }
75
89
 
76
90
  const start = () => document.querySelectorAll('script[type="text/ruby"][data-webvas]').forEach(element => {
data/lib/webvas/bridge.rb CHANGED
@@ -37,5 +37,10 @@ module Webvas
37
37
  def show_error(message, backtrace)
38
38
  @api.showError(String(message), Array(backtrace).join("\n"))
39
39
  end
40
+
41
+ def run_shader(canvas, source, layout, uniforms)
42
+ @api.shaderMode(canvas)
43
+ JS.global[:GlazeWGSL].run(canvas, source, layout, uniforms)
44
+ end
40
45
  end
41
46
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Webvas
6
+ class Shader
7
+ DEFAULT_LAYOUT = {
8
+ fields: {
9
+ resolution: { offset: 0, type: :vec2 },
10
+ time: { offset: 8, type: :float },
11
+ frame: { offset: 12, type: :int },
12
+ mouse: { offset: 16, type: :vec4 }
13
+ },
14
+ size: 32
15
+ }.freeze
16
+
17
+ attr_reader :source, :canvas, :layout
18
+
19
+ def initialize(source, canvas: "#screen", layout: nil)
20
+ @source = String(source)
21
+ @canvas = String(canvas)
22
+ @layout = layout || DEFAULT_LAYOUT
23
+ end
24
+
25
+ def run(canvas: @canvas, bridge: Bridge.new, &uniforms)
26
+ stop
27
+ callback = proc { |time| JSON.generate(uniforms ? uniforms.call(Float(time)) : {}) }
28
+ @runner = bridge.run_shader(String(canvas), @source, JSON.generate(@layout), callback)
29
+ end
30
+
31
+ def stop
32
+ @runner.stop if @runner
33
+ @runner = nil
34
+ end
35
+ end
36
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webvas
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/webvas.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "webvas/backend"
7
7
  require_relative "webvas/input"
8
8
  require_relative "webvas/key_map"
9
9
  require_relative "webvas/runner"
10
+ require_relative "webvas/shader"
10
11
 
11
12
  module Webvas
12
13
  class Error < StandardError; end
@@ -36,4 +37,9 @@ module Webvas
36
37
  Bridge.new.show_error(error.message, error.backtrace || [])
37
38
  end
38
39
 
40
+ def self.run_shader(shader, canvas: "#gpu", &uniforms)
41
+ raise ArgumentError, "shader must be a Webvas::Shader" unless shader.is_a?(Shader)
42
+
43
+ shader.run(canvas:, &uniforms)
44
+ end
39
45
  end
@@ -0,0 +1,9 @@
1
+ --- a/ext/psych/extconf.rb
2
+ +++ b/ext/psych/extconf.rb
3
+ @@ -40,3 +40,3 @@
4
+ # default to pre-installed libyaml
5
+ -elsif pkg_config('yaml-0.1')
6
+ +elsif !CROSS_COMPILING && pkg_config('yaml-0.1')
7
+ # found with pkg-config
8
+ else
9
+ dir_config('libyaml')
data/runtime/Gemfile ADDED
@@ -0,0 +1,22 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "ruby_wasm", "~> 2.10.1"
4
+ gem "js", "~> 2.10.1"
5
+
6
+ [
7
+ ["webvas", ".."],
8
+ ["glaze", "../../glaze"],
9
+ ["gesso", "../../gesso"],
10
+ ["glyphic", "../../glyphic"],
11
+ ["larb", "../../larb"],
12
+ ["rbgl", "../../rbgl"],
13
+ ["rlsl", "../../rlsl"],
14
+ ["tessel", "../../tessel"]
15
+ ].each do |name, relative_path|
16
+ path = File.expand_path(relative_path, __dir__)
17
+ if File.file?(File.join(path, "#{name}.gemspec"))
18
+ gem name, path: path
19
+ else
20
+ gem name
21
+ end
22
+ end
@@ -0,0 +1,101 @@
1
+ PATH
2
+ remote: ../../gesso
3
+ specs:
4
+ gesso (0.1.0)
5
+ glyphic (>= 0.1.0)
6
+ larb (>= 1.0.0)
7
+ rbgl (>= 1.0.0)
8
+ rlsl (>= 1.0.0)
9
+ tessel (>= 0.1.0)
10
+
11
+ PATH
12
+ remote: ../../glaze
13
+ specs:
14
+ glaze (0.1.0)
15
+ rbgl (>= 1.0.0)
16
+ rlsl (>= 1.0.0)
17
+ tessel (>= 0.1.0)
18
+
19
+ PATH
20
+ remote: ../../glyphic
21
+ specs:
22
+ glyphic (0.1.0)
23
+ tessel (>= 0.1.0)
24
+
25
+ PATH
26
+ remote: ../../larb
27
+ specs:
28
+ larb (1.0.1)
29
+
30
+ PATH
31
+ remote: ../../rbgl
32
+ specs:
33
+ rbgl (1.0.0)
34
+ larb (~> 1.0)
35
+
36
+ PATH
37
+ remote: ../../rlsl
38
+ specs:
39
+ rlsl (1.0.0)
40
+ prism (>= 1.0.0)
41
+
42
+ PATH
43
+ remote: ../../tessel
44
+ specs:
45
+ tessel (0.2.0)
46
+
47
+ PATH
48
+ remote: ..
49
+ specs:
50
+ webvas (0.3.0)
51
+ base64 (~> 0.2)
52
+ glaze (>= 0.1.0, < 0.2)
53
+ rbgl (>= 1.0.0, < 2)
54
+
55
+ GEM
56
+ remote: https://rubygems.org/
57
+ specs:
58
+ base64 (0.3.0)
59
+ js (2.10.1)
60
+ logger (1.7.0)
61
+ prism (1.9.0)
62
+ ruby_wasm (2.10.1-arm64-darwin)
63
+ logger
64
+ ruby_wasm (2.10.1-x86_64-linux)
65
+ logger
66
+
67
+ PLATFORMS
68
+ arm64-darwin-24
69
+ arm64-darwin-25
70
+ x86_64-linux
71
+
72
+ DEPENDENCIES
73
+ gesso!
74
+ glaze!
75
+ glyphic!
76
+ js (~> 2.10.1)
77
+ larb!
78
+ rbgl!
79
+ rlsl!
80
+ ruby_wasm (~> 2.10.1)
81
+ tessel!
82
+ webvas!
83
+
84
+ CHECKSUMS
85
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
86
+ gesso (0.1.0)
87
+ glaze (0.1.0)
88
+ glyphic (0.1.0)
89
+ js (2.10.1) sha256=c5fb88c60ea936234f818ff240f2ab0c42a57c25ab9b575211fcc4d702d059fb
90
+ larb (1.0.1)
91
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
92
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
93
+ rbgl (1.0.0)
94
+ rlsl (1.0.0)
95
+ ruby_wasm (2.10.1-arm64-darwin) sha256=f106a9b4d8b273592471005460f8c54d89a8913e4fecb9dbab3bd3fc8c20bbc6
96
+ ruby_wasm (2.10.1-x86_64-linux) sha256=728e29dc688246572b56dcef1c7b131431d07865320c9b8434dd1ccdfd63db2b
97
+ tessel (0.2.0)
98
+ webvas (0.3.0)
99
+
100
+ BUNDLED WITH
101
+ 2.6.9
data/script/build_site ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "cgi"
6
+ require "rubygems"
7
+ require "uri"
8
+ require "zlib"
9
+
10
+ root = File.expand_path("..", __dir__)
11
+ destination = File.expand_path(ARGV.fetch(0, File.join(root, "public")))
12
+ runtime_url = ARGV.fetch(1, "./assets/webvas.wasm")
13
+ wasm = ARGV[2] || File.join(root, "build", "webvas.wasm")
14
+ uri = URI.parse(runtime_url)
15
+ if uri.host && (uri.scheme != "https" || uri.userinfo)
16
+ abort "Runtime URL must use HTTPS"
17
+ end
18
+ glaze_runner = File.expand_path("../glaze/lib/glaze/webgpu_runner.js", root)
19
+ unless File.file?(glaze_runner)
20
+ glaze = Gem::Specification.find_by_name("glaze")
21
+ glaze_runner = File.join(glaze.full_gem_path, "lib", "glaze", "webgpu_runner.js")
22
+ end
23
+
24
+ abort "Build the WebAssembly runtime first: #{wasm}" if runtime_url.start_with?(".", "/") && !File.file?(wasm)
25
+ abort "Missing shared Glaze WebGPU runner: #{glaze_runner}" unless File.file?(glaze_runner)
26
+
27
+ FileUtils.rm_rf(destination)
28
+ FileUtils.cp_r(File.join(root, "site"), destination)
29
+ FileUtils.cp(File.join(root, "js", "bridge.js"), File.join(destination, "bridge.js"))
30
+ FileUtils.cp(File.join(root, "js", "loader.js"), File.join(destination, "loader.js"))
31
+ FileUtils.cp(glaze_runner, File.join(destination, "wgsl-runner.js"))
32
+ index = File.join(destination, "index.html")
33
+ html = File.read(index)
34
+ html.sub!(%r{(<meta name="webvas-runtime" content=")[^"]*(">)}, "\\1#{CGI.escapeHTML(runtime_url)}\\2")
35
+ if uri.host
36
+ html.sub!(/connect-src ([^;]+)/) do
37
+ sources = Regexp.last_match(1)
38
+ "connect-src #{sources} https://#{uri.host}"
39
+ end
40
+ end
41
+ File.write(index, html)
42
+
43
+ if runtime_url.start_with?(".", "/")
44
+ abort "Build the WebAssembly runtime first: #{wasm}" unless File.file?(wasm)
45
+ FileUtils.mkdir_p(File.join(destination, "assets"))
46
+ FileUtils.cp(wasm, File.join(destination, "assets", "webvas.wasm"))
47
+ raw_size = File.size(wasm)
48
+ compressed_size = Zlib::Deflate.deflate(File.binread(wasm), Zlib::BEST_COMPRESSION).bytesize
49
+ puts "Webvas runtime: #{raw_size} bytes (#{compressed_size} deflate bytes)"
50
+ else
51
+ puts "Webvas runtime: #{runtime_url}"
52
+ end
data/sig/webvas.rbs CHANGED
@@ -13,6 +13,7 @@ module Webvas
13
13
  def close: (untyped handle) -> untyped
14
14
  def request_animation_frame: (Proc callback) -> untyped
15
15
  def show_error: (String message, Array[String] backtrace) -> untyped
16
+ def run_shader: (String canvas, String source, String layout, Proc uniforms) -> untyped
16
17
  end
17
18
 
18
19
  class Backend < RBGL::GUI::Backend
@@ -43,6 +44,16 @@ module Webvas
43
44
  def request_animation_frame: (Proc callback) -> untyped
44
45
  end
45
46
 
47
+ class Shader
48
+ attr_reader source: String
49
+ attr_reader canvas: String
50
+ attr_reader layout: Hash[Symbol, untyped]
51
+ def initialize: (String source, ?canvas: String, ?layout: Hash[Symbol, untyped]) -> void
52
+ def run: (?canvas: String, ?bridge: Bridge) ?{ (Float) -> Hash[Symbol, untyped] } -> untyped
53
+ def stop: () -> nil
54
+ end
55
+
46
56
  def self.run: (RBGL::GUI::Window window, ?scheduler: Scheduler, ?on_error: Proc) { (RBGL::Engine::Context, Float) -> void } -> Proc
57
+ def self.run_shader: (Shader shader, ?canvas: String) ?{ (Float) -> Hash[Symbol, untyped] } -> untyped
47
58
  def self.report_error: (StandardError error) -> untyped
48
59
  end