webvas 0.3.0 → 0.3.1
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/CHANGELOG.md +16 -5
- data/LICENSE.txt +1 -1
- data/README.md +68 -77
- data/exe/webvas +3 -267
- data/js/bridge.js +117 -19
- data/js/loader.js +114 -52
- data/{site → js}/worker.js +2 -30
- data/lib/webvas/backend.rb +8 -1
- data/lib/webvas/bridge.rb +4 -5
- data/lib/webvas/cli.rb +319 -0
- data/lib/webvas/shader.rb +4 -27
- data/lib/webvas/version.rb +1 -1
- data/lib/webvas.rb +4 -3
- data/sig/webvas.rbs +8 -11
- metadata +5 -35
- data/docs/performance.md +0 -12
- data/docs/security.md +0 -9
- data/docs/spikes.md +0 -18
- data/runtime/Gemfile +0 -22
- data/runtime/Gemfile.lock +0 -101
- data/script/build_site +0 -52
- data/site/app.js +0 -466
- data/site/index.html +0 -56
- data/site/loader-example.html +0 -26
- data/site/style.css +0 -89
data/runtime/Gemfile.lock
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
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/site/app.js
DELETED
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
const editor = document.querySelector("#source");
|
|
2
|
-
const stage = document.querySelector("#stage");
|
|
3
|
-
const status = document.querySelector("#status");
|
|
4
|
-
const consoleBox = document.querySelector(".console");
|
|
5
|
-
const loading = document.querySelector("#loading");
|
|
6
|
-
const loadingLabel = document.querySelector("#loading-label");
|
|
7
|
-
const dirty = document.querySelector("#dirty");
|
|
8
|
-
let worker;
|
|
9
|
-
let pendingSource;
|
|
10
|
-
let screen;
|
|
11
|
-
let gpu;
|
|
12
|
-
let generation = 0;
|
|
13
|
-
let workerBlobUrl;
|
|
14
|
-
const PLAYGROUND_URL = "https://rbgfx.github.io/webvas/";
|
|
15
|
-
|
|
16
|
-
function shaderExample(label, name, fragment) {
|
|
17
|
-
return {
|
|
18
|
-
name: label,
|
|
19
|
-
source: [
|
|
20
|
-
'require "webvas"',
|
|
21
|
-
'require "rlsl"',
|
|
22
|
-
"",
|
|
23
|
-
`builder = RLSL::ShaderBuilder.new(:${name})`,
|
|
24
|
-
"builder.uniforms { float :time }",
|
|
25
|
-
"builder.fragment_source <<~SHADER",
|
|
26
|
-
...fragment.trim().split("\n").map(line => " " + line),
|
|
27
|
-
"SHADER",
|
|
28
|
-
"layout = RLSL::WGSL::UniformLayout.build({ resolution: :vec2, time: :float })",
|
|
29
|
-
'shader = Webvas::Shader.new(builder.build_wgsl_shader, canvas: "#gpu", layout: layout)',
|
|
30
|
-
"Webvas.run_shader(shader) { |time| { time: time } }",
|
|
31
|
-
""
|
|
32
|
-
].join("\n")
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const examples = [
|
|
37
|
-
{
|
|
38
|
-
name: "Gesso · animated orbit",
|
|
39
|
-
source: [
|
|
40
|
-
'require "gesso"',
|
|
41
|
-
"",
|
|
42
|
-
'Gesso.run(width: 480, height: 320, runner: :web) do',
|
|
43
|
-
' background "#171a18"',
|
|
44
|
-
" draw do",
|
|
45
|
-
' background "#171a18"',
|
|
46
|
-
" no_stroke",
|
|
47
|
-
' fill "#d38a62"',
|
|
48
|
-
" circle 240 + Math.cos(millis / 850.0) * 92, 160, 18",
|
|
49
|
-
' fill "#718b74"',
|
|
50
|
-
" circle 240 + Math.cos(millis / 850.0 + Math::PI) * 92, 160, 11",
|
|
51
|
-
' fill "#d9ddd5"',
|
|
52
|
-
" circle 240, 160, 4",
|
|
53
|
-
" end",
|
|
54
|
-
"end",
|
|
55
|
-
""
|
|
56
|
-
].join("\n")
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
name: "Gesso · orbiting bubbles",
|
|
60
|
-
source: [
|
|
61
|
-
'require "gesso"',
|
|
62
|
-
"",
|
|
63
|
-
'Gesso.run(width: 480, height: 320, runner: :web) do',
|
|
64
|
-
' background "#101820"',
|
|
65
|
-
" draw do",
|
|
66
|
-
' background "#101820"',
|
|
67
|
-
" no_stroke",
|
|
68
|
-
" 12.times do |index|",
|
|
69
|
-
" angle = millis / 1100.0 + index * Math::PI * 2 / 12",
|
|
70
|
-
' fill index.even? ? "#79b4a8" : "#e6b566"',
|
|
71
|
-
" circle 240 + Math.cos(angle) * 110, 160 + Math.sin(angle * 1.3) * 72, 7 + index % 4 * 2",
|
|
72
|
-
" end",
|
|
73
|
-
" end",
|
|
74
|
-
"end",
|
|
75
|
-
""
|
|
76
|
-
].join("\n")
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: "Gesso · pointer light",
|
|
80
|
-
source: [
|
|
81
|
-
'require "gesso"',
|
|
82
|
-
"",
|
|
83
|
-
'Gesso.run(width: 480, height: 320, runner: :web) do',
|
|
84
|
-
' background "#111417"',
|
|
85
|
-
" draw do",
|
|
86
|
-
' background "#111417"',
|
|
87
|
-
" no_stroke",
|
|
88
|
-
' fill mouse_pressed? ? "#e5b36a" : "#84a99d"',
|
|
89
|
-
" circle mouse_x, mouse_y, mouse_pressed? ? 32 : 18",
|
|
90
|
-
' fill "#e5e6df"',
|
|
91
|
-
" circle mouse_x, mouse_y, 3",
|
|
92
|
-
" end",
|
|
93
|
-
"end",
|
|
94
|
-
""
|
|
95
|
-
].join("\n")
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: "RBGL · RGB triangle",
|
|
99
|
-
source: [
|
|
100
|
-
'require "webvas"',
|
|
101
|
-
'require "rbgl"',
|
|
102
|
-
"include Larb",
|
|
103
|
-
"include RBGL::Engine",
|
|
104
|
-
"",
|
|
105
|
-
"backend = Webvas::Backend.new(width: 480, height: 320)",
|
|
106
|
-
"window = RBGL::GUI::Window.new(width: 480, height: 320, backend: backend)",
|
|
107
|
-
"pipeline = Pipeline.create do",
|
|
108
|
-
" vertex do |input, _uniforms, output|",
|
|
109
|
-
" output.position = input.position.to_vec4",
|
|
110
|
-
" output.color = input.color",
|
|
111
|
-
" end",
|
|
112
|
-
" fragment do |input, _uniforms, output|",
|
|
113
|
-
" output.color = input.color",
|
|
114
|
-
" end",
|
|
115
|
-
"end",
|
|
116
|
-
"pipeline.cull_mode = :none",
|
|
117
|
-
"vertices = VertexBuffer.from_array(VertexLayout.position_color, [",
|
|
118
|
-
" { position: Vec3[0, 0.7, 0], color: Color.red },",
|
|
119
|
-
" { position: Vec3[-0.7, -0.7, 0], color: Color.green },",
|
|
120
|
-
" { position: Vec3[0.7, -0.7, 0], color: Color.blue }",
|
|
121
|
-
"]) ",
|
|
122
|
-
"Webvas.run(window) do |context, _delta|",
|
|
123
|
-
' context.clear(color: Color.from_hex("#101827"))',
|
|
124
|
-
" context.bind_pipeline(pipeline)",
|
|
125
|
-
" context.bind_vertex_buffer(vertices)",
|
|
126
|
-
" context.draw_arrays(:triangles, 0, 3)",
|
|
127
|
-
"end",
|
|
128
|
-
""
|
|
129
|
-
].join("\n")
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
name: "RBGL · clear color",
|
|
133
|
-
source: [
|
|
134
|
-
'require "webvas"',
|
|
135
|
-
'require "rbgl"',
|
|
136
|
-
"include Larb",
|
|
137
|
-
"",
|
|
138
|
-
"backend = Webvas::Backend.new(width: 480, height: 320)",
|
|
139
|
-
"window = RBGL::GUI::Window.new(width: 480, height: 320, backend: backend)",
|
|
140
|
-
"Webvas.run(window) do |context, _delta|",
|
|
141
|
-
' context.clear(color: Color.from_hex("#293b4a"))',
|
|
142
|
-
"end",
|
|
143
|
-
""
|
|
144
|
-
].join("\n")
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
name: "RBGL · pixel gradient",
|
|
148
|
-
source: [
|
|
149
|
-
'require "webvas"',
|
|
150
|
-
'require "rbgl"',
|
|
151
|
-
"width, height = 480, 320",
|
|
152
|
-
'pixels = String.new(capacity: width * height * 4, encoding: Encoding::BINARY)',
|
|
153
|
-
"height.times do |y|",
|
|
154
|
-
' width.times { |x| pixels << [x * 255 / width, y * 255 / height, 120, 255].pack("C4") }',
|
|
155
|
-
"end",
|
|
156
|
-
"backend = Webvas::Backend.new(width: width, height: height)",
|
|
157
|
-
"window = RBGL::GUI::Window.new(width: width, height: height, backend: backend)",
|
|
158
|
-
"Webvas.run(window) { window.set_pixels(pixels) }",
|
|
159
|
-
""
|
|
160
|
-
].join("\n")
|
|
161
|
-
},
|
|
162
|
-
shaderExample("RLSL · animated plasma", "webvas_plasma", `
|
|
163
|
-
uv = frag_coord / resolution.y
|
|
164
|
-
wave = sin(uv.x * 8.0 + u.time) * cos(uv.y * 7.0 - u.time)
|
|
165
|
-
vec3(0.5 + wave * 0.25, 0.25 + uv.y * 0.5, 0.5 - wave * 0.3)
|
|
166
|
-
`),
|
|
167
|
-
shaderExample("RLSL · orbiting rings", "webvas_rings", `
|
|
168
|
-
uv = (frag_coord - resolution * 0.5) / resolution.y
|
|
169
|
-
radius = sqrt(uv.x * uv.x + uv.y * uv.y)
|
|
170
|
-
wave = 0.5 + 0.5 * cos(radius * 42.0 - u.time * 2.0)
|
|
171
|
-
vec3(wave * 0.8, 0.25 + uv.y * 0.3, 1.0 - wave * 0.6)
|
|
172
|
-
`),
|
|
173
|
-
shaderExample("RLSL · shifting gradient", "webvas_gradient", `
|
|
174
|
-
uv = frag_coord / resolution
|
|
175
|
-
vec3(0.15 + uv.x * 0.6, 0.2 + uv.y * 0.6, 0.45 + 0.2 * sin(u.time))
|
|
176
|
-
`)
|
|
177
|
-
];
|
|
178
|
-
|
|
179
|
-
function setStatus(message, error = false) {
|
|
180
|
-
status.textContent = message;
|
|
181
|
-
consoleBox.classList.toggle("error", error);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function setSource(value, markDirty = true) {
|
|
185
|
-
editor.value = value;
|
|
186
|
-
dirty.classList.toggle("visible", markDirty);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function selectErrorLine(backtrace) {
|
|
190
|
-
const line = Number(backtrace.match(/\beval:(\d+):/)?.[1]);
|
|
191
|
-
if (!line) return;
|
|
192
|
-
let start = 0;
|
|
193
|
-
for (let index = 1; index < line; index += 1) {
|
|
194
|
-
const newline = editor.value.indexOf("\n", start);
|
|
195
|
-
if (newline < 0) return;
|
|
196
|
-
start = newline + 1;
|
|
197
|
-
}
|
|
198
|
-
const newline = editor.value.indexOf("\n", start);
|
|
199
|
-
editor.focus({ preventScroll: true });
|
|
200
|
-
editor.setSelectionRange(start, newline < 0 ? editor.value.length : newline);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function mountCanvases() {
|
|
204
|
-
screen = document.createElement("canvas");
|
|
205
|
-
gpu = document.createElement("canvas");
|
|
206
|
-
screen.id = "screen";
|
|
207
|
-
gpu.id = "gpu";
|
|
208
|
-
screen.width = gpu.width = 480;
|
|
209
|
-
screen.height = gpu.height = 320;
|
|
210
|
-
screen.tabIndex = gpu.tabIndex = 0;
|
|
211
|
-
screen.setAttribute("aria-label", "Ruby 2D sketch canvas");
|
|
212
|
-
gpu.setAttribute("aria-label", "Ruby WebGPU shader canvas");
|
|
213
|
-
gpu.hidden = true;
|
|
214
|
-
stage.replaceChildren(screen, gpu);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function start(source) {
|
|
218
|
-
const currentGeneration = ++generation;
|
|
219
|
-
pendingSource = source;
|
|
220
|
-
worker?.terminate();
|
|
221
|
-
if (workerBlobUrl) URL.revokeObjectURL(workerBlobUrl);
|
|
222
|
-
worker = null;
|
|
223
|
-
mountCanvases();
|
|
224
|
-
loading.classList.remove("hidden");
|
|
225
|
-
loadingLabel.textContent = "Starting an isolated Ruby runtime…";
|
|
226
|
-
document.querySelector("#runtime-state").textContent = "Starting worker";
|
|
227
|
-
document.querySelector("#performance-state").textContent = "Frame timing warming up";
|
|
228
|
-
setStatus("Starting Ruby runtime…");
|
|
229
|
-
try {
|
|
230
|
-
const workerUrl = new URL("./worker.js", import.meta.url);
|
|
231
|
-
const response = await fetch(workerUrl, { credentials: "omit" });
|
|
232
|
-
if (!response.ok) throw new Error("Worker download failed: " + response.status);
|
|
233
|
-
if (generation !== currentGeneration) return;
|
|
234
|
-
workerBlobUrl = URL.createObjectURL(new Blob([await response.text()], { type: "text/javascript" }));
|
|
235
|
-
worker = new Worker(workerBlobUrl, { type: "module", name: "webvas" });
|
|
236
|
-
} catch (error) {
|
|
237
|
-
if (generation === currentGeneration) {
|
|
238
|
-
loading.classList.add("hidden");
|
|
239
|
-
setStatus(error.message || "Worker could not start", true);
|
|
240
|
-
}
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
const active = worker;
|
|
244
|
-
worker.addEventListener("message", event => {
|
|
245
|
-
if (generation !== currentGeneration || worker !== active) return;
|
|
246
|
-
const message = event.data || {};
|
|
247
|
-
if (message.type === "webvas:loading") {
|
|
248
|
-
loadingLabel.textContent = message.message;
|
|
249
|
-
document.querySelector("#runtime-state").textContent = "Loading runtime";
|
|
250
|
-
} else if (message.type === "webvas:ready") {
|
|
251
|
-
if (workerBlobUrl) URL.revokeObjectURL(workerBlobUrl);
|
|
252
|
-
workerBlobUrl = null;
|
|
253
|
-
loading.classList.add("hidden");
|
|
254
|
-
document.querySelector("#runtime-state").textContent = "Ruby ready";
|
|
255
|
-
active.postMessage({ type: "webvas:run", source: pendingSource });
|
|
256
|
-
pendingSource = null;
|
|
257
|
-
} else if (message.type === "webvas:started") {
|
|
258
|
-
dirty.classList.remove("visible");
|
|
259
|
-
setStatus("Sketch is running");
|
|
260
|
-
} else if (message.type === "webvas:mode") {
|
|
261
|
-
screen.hidden = message.canvas === "#gpu";
|
|
262
|
-
gpu.hidden = message.canvas !== "#gpu";
|
|
263
|
-
} else if (message.type === "webvas:pixelated") {
|
|
264
|
-
const canvas = message.selector === "#gpu" ? gpu : screen;
|
|
265
|
-
canvas.style.imageRendering = message.enabled ? "pixelated" : "auto";
|
|
266
|
-
} else if (message.type === "webvas:size") {
|
|
267
|
-
document.querySelector("#canvas-size").textContent = message.width + " × " + message.height;
|
|
268
|
-
} else if (message.type === "webvas:metrics") {
|
|
269
|
-
document.querySelector("#performance-state").textContent =
|
|
270
|
-
`${message.fps.toFixed(1)} fps · ${message.callbackMs.toFixed(2)} ms / frame`;
|
|
271
|
-
} else if (message.type === "webvas:error") {
|
|
272
|
-
loading.classList.add("hidden");
|
|
273
|
-
selectErrorLine(message.backtrace || "");
|
|
274
|
-
setStatus([message.message, message.backtrace].filter(Boolean).join("\n"), true);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
worker.addEventListener("error", event => {
|
|
278
|
-
if (generation !== currentGeneration || worker !== active) return;
|
|
279
|
-
loading.classList.add("hidden");
|
|
280
|
-
const reason = event.error?.stack || event.error?.message || event.message;
|
|
281
|
-
setStatus(reason || `Worker failed${event.filename ? `: ${event.filename}:${event.lineno}` : ""}`, true);
|
|
282
|
-
});
|
|
283
|
-
const screenCanvas = screen.transferControlToOffscreen();
|
|
284
|
-
const gpuCanvas = gpu.transferControlToOffscreen();
|
|
285
|
-
worker.postMessage({
|
|
286
|
-
type: "webvas:init",
|
|
287
|
-
screen: screenCanvas,
|
|
288
|
-
gpu: gpuCanvas,
|
|
289
|
-
bridgeUrl: new URL("./bridge.js", import.meta.url).href,
|
|
290
|
-
shaderUrl: new URL("./wgsl-runner.js", import.meta.url).href,
|
|
291
|
-
wasmUrl: new URL(document.querySelector('meta[name="webvas-runtime"]').content, document.baseURI).href
|
|
292
|
-
}, [screenCanvas, gpuCanvas]);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function sendInput(event) {
|
|
296
|
-
if (!worker || !["pointerdown", "pointerup", "pointermove", "pointercancel", "keydown", "keyup", "wheel"].includes(event.type)) return;
|
|
297
|
-
const rect = event.target.getBoundingClientRect();
|
|
298
|
-
if (event.type === "pointerdown") {
|
|
299
|
-
event.target.focus({ preventScroll: true });
|
|
300
|
-
event.target.setPointerCapture(event.pointerId);
|
|
301
|
-
}
|
|
302
|
-
if (event.type === "wheel") event.preventDefault();
|
|
303
|
-
if (["keydown", "keyup"].includes(event.type) && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "].includes(event.key)) event.preventDefault();
|
|
304
|
-
worker.postMessage({
|
|
305
|
-
type: "webvas:input",
|
|
306
|
-
event: {
|
|
307
|
-
type: event.type === "pointercancel" ? "pointerup" : event.type,
|
|
308
|
-
clientX: event.clientX || 0,
|
|
309
|
-
clientY: event.clientY || 0,
|
|
310
|
-
left: rect.left,
|
|
311
|
-
top: rect.top,
|
|
312
|
-
rectWidth: rect.width,
|
|
313
|
-
rectHeight: rect.height,
|
|
314
|
-
button: event.button,
|
|
315
|
-
deltaX: event.deltaX,
|
|
316
|
-
deltaY: event.deltaY,
|
|
317
|
-
code: event.code,
|
|
318
|
-
key: event.key,
|
|
319
|
-
shiftKey: event.shiftKey,
|
|
320
|
-
ctrlKey: event.ctrlKey,
|
|
321
|
-
altKey: event.altKey,
|
|
322
|
-
metaKey: event.metaKey
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function share() {
|
|
328
|
-
try {
|
|
329
|
-
if (!("CompressionStream" in window) || !navigator.clipboard) {
|
|
330
|
-
throw new Error("Share links require a modern browser and clipboard permission.");
|
|
331
|
-
}
|
|
332
|
-
const source = new TextEncoder().encode(editor.value);
|
|
333
|
-
if (source.length > 32768) throw new Error("Share links are limited to 32 KB of source.");
|
|
334
|
-
const stream = new Blob([source]).stream().pipeThrough(new CompressionStream("deflate"));
|
|
335
|
-
const compressed = new Uint8Array(await new Response(stream).arrayBuffer());
|
|
336
|
-
let binary = "";
|
|
337
|
-
compressed.forEach(byte => { binary += String.fromCharCode(byte); });
|
|
338
|
-
const token = btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
339
|
-
const url = new URL(location.href);
|
|
340
|
-
url.searchParams.delete("source");
|
|
341
|
-
url.hash = "code=" + token;
|
|
342
|
-
await navigator.clipboard.writeText(url.href);
|
|
343
|
-
setStatus("Share link copied");
|
|
344
|
-
} catch (error) {
|
|
345
|
-
setStatus(error.message || "Could not create a share link", true);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function download() {
|
|
350
|
-
const url = URL.createObjectURL(new Blob([editor.value], { type: "text/x-ruby;charset=utf-8" }));
|
|
351
|
-
const link = Object.assign(document.createElement("a"), { href: url, download: "sketch.rb" });
|
|
352
|
-
link.click();
|
|
353
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function downloadHtml() {
|
|
357
|
-
if (new TextEncoder().encode(editor.value).length > 32768) {
|
|
358
|
-
setStatus("HTML downloads are limited to 32 KB of source.", true);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
const page = document.documentElement.cloneNode(true);
|
|
362
|
-
page.querySelector("#source").textContent = editor.value;
|
|
363
|
-
const stylesheet = page.querySelector('link[rel="stylesheet"]');
|
|
364
|
-
stylesheet.href = new URL(stylesheet.getAttribute("href"), PLAYGROUND_URL).href;
|
|
365
|
-
const entry = page.querySelector('script[type="module"]');
|
|
366
|
-
entry.src = new URL(entry.getAttribute("src"), PLAYGROUND_URL).href;
|
|
367
|
-
page.querySelector('meta[name="webvas-runtime"]').content = new URL("./assets/webvas.wasm", PLAYGROUND_URL).href;
|
|
368
|
-
page.querySelector('meta[http-equiv="Content-Security-Policy"]').content =
|
|
369
|
-
"default-src 'none'; script-src https://rbgfx.github.io https://cdn.jsdelivr.net 'wasm-unsafe-eval' 'unsafe-eval'; worker-src blob:; connect-src https://rbgfx.github.io data:; style-src https://rbgfx.github.io; img-src https://rbgfx.github.io data: blob:; font-src https://rbgfx.github.io; object-src 'none'; base-uri 'none'";
|
|
370
|
-
const url = URL.createObjectURL(new Blob(["<!doctype html>\n", page.outerHTML], { type: "text/html" }));
|
|
371
|
-
const link = Object.assign(document.createElement("a"), { href: url, download: "webvas-sketch.html" });
|
|
372
|
-
link.click();
|
|
373
|
-
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function loadSharedSource() {
|
|
377
|
-
const token = new URLSearchParams(location.hash.slice(1)).get("code");
|
|
378
|
-
if (!token) return;
|
|
379
|
-
try {
|
|
380
|
-
if (token.length > 65536) throw new Error("Shared link is too large.");
|
|
381
|
-
const normalized = token.replaceAll("-", "+").replaceAll("_", "/");
|
|
382
|
-
const binary = atob(normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="));
|
|
383
|
-
const bytes = Uint8Array.from(binary, char => char.charCodeAt(0));
|
|
384
|
-
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate"));
|
|
385
|
-
const source = await readLimited(stream, 32768);
|
|
386
|
-
setSource(source, false);
|
|
387
|
-
history.replaceState(null, "", location.pathname + location.search);
|
|
388
|
-
} catch (error) {
|
|
389
|
-
setStatus(error.message || "Could not decode the shared sketch", true);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function readLimited(stream, limit) {
|
|
394
|
-
const reader = stream.getReader();
|
|
395
|
-
const chunks = [];
|
|
396
|
-
let size = 0;
|
|
397
|
-
while (true) {
|
|
398
|
-
const { done, value } = await reader.read();
|
|
399
|
-
if (done) break;
|
|
400
|
-
size += value.byteLength;
|
|
401
|
-
if (size > limit) {
|
|
402
|
-
await reader.cancel();
|
|
403
|
-
throw new Error("Source exceeds 32 KB.");
|
|
404
|
-
}
|
|
405
|
-
chunks.push(value);
|
|
406
|
-
}
|
|
407
|
-
const bytes = new Uint8Array(size);
|
|
408
|
-
let offset = 0;
|
|
409
|
-
for (const chunk of chunks) {
|
|
410
|
-
bytes.set(chunk, offset);
|
|
411
|
-
offset += chunk.byteLength;
|
|
412
|
-
}
|
|
413
|
-
return new TextDecoder().decode(bytes);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async function loadProjectSource() {
|
|
417
|
-
const path = new URLSearchParams(location.search).get("source");
|
|
418
|
-
if (!path) return;
|
|
419
|
-
try {
|
|
420
|
-
const base = new URL(".", location.href);
|
|
421
|
-
const url = new URL(path, base);
|
|
422
|
-
if (url.origin !== base.origin || !url.pathname.startsWith(base.pathname)) {
|
|
423
|
-
throw new Error("Project source must be on this site.");
|
|
424
|
-
}
|
|
425
|
-
const response = await fetch(url, { credentials: "omit" });
|
|
426
|
-
if (!response.ok) throw new Error("Project source could not be loaded.");
|
|
427
|
-
const source = await readLimited(response.body, 32768);
|
|
428
|
-
setSource(source, false);
|
|
429
|
-
} catch (error) {
|
|
430
|
-
setStatus(error.message || "Could not load project source", true);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
for (const [index, example] of examples.entries()) {
|
|
435
|
-
const option = document.createElement("option");
|
|
436
|
-
option.value = String(index);
|
|
437
|
-
option.textContent = example.name;
|
|
438
|
-
document.querySelector("#examples").append(option);
|
|
439
|
-
}
|
|
440
|
-
document.querySelector("#examples").addEventListener("change", event => {
|
|
441
|
-
const example = examples[Number(event.target.value)];
|
|
442
|
-
if (example) setSource(example.source);
|
|
443
|
-
event.target.value = "";
|
|
444
|
-
});
|
|
445
|
-
document.querySelector("#run").addEventListener("click", () => start(editor.value));
|
|
446
|
-
document.querySelector("#share").addEventListener("click", share);
|
|
447
|
-
document.querySelector("#download").addEventListener("click", download);
|
|
448
|
-
document.querySelector("#download-html").addEventListener("click", downloadHtml);
|
|
449
|
-
editor.addEventListener("input", () => dirty.classList.add("visible"));
|
|
450
|
-
editor.addEventListener("keydown", event => {
|
|
451
|
-
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
452
|
-
event.preventDefault();
|
|
453
|
-
start(editor.value);
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
stage.addEventListener("pointerdown", sendInput);
|
|
457
|
-
stage.addEventListener("pointerup", sendInput);
|
|
458
|
-
stage.addEventListener("pointermove", sendInput);
|
|
459
|
-
stage.addEventListener("pointercancel", sendInput);
|
|
460
|
-
stage.addEventListener("keydown", sendInput);
|
|
461
|
-
stage.addEventListener("keyup", sendInput);
|
|
462
|
-
stage.addEventListener("wheel", sendInput, { passive: false });
|
|
463
|
-
setSource(editor.value || examples[0].source, false);
|
|
464
|
-
await loadProjectSource();
|
|
465
|
-
await loadSharedSource();
|
|
466
|
-
start(editor.value);
|
data/site/index.html
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<meta name="color-scheme" content="dark">
|
|
7
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://rbgfx.github.io https://cdn.jsdelivr.net 'wasm-unsafe-eval' 'unsafe-eval'; worker-src blob:; connect-src 'self' https://rbgfx.github.io data:; style-src 'self' https://rbgfx.github.io; img-src 'self' https://rbgfx.github.io data: blob:; font-src 'self' https://rbgfx.github.io; object-src 'none'; base-uri 'self'">
|
|
8
|
-
<meta name="webvas-runtime" content="./assets/webvas.wasm">
|
|
9
|
-
<meta name="description" content="Run Ruby sketches in your browser with RBGL, Gesso, and ruby.wasm.">
|
|
10
|
-
<title>Webvas — Ruby, in motion</title>
|
|
11
|
-
<link rel="stylesheet" href="./style.css">
|
|
12
|
-
</head>
|
|
13
|
-
<body>
|
|
14
|
-
<header class="topbar">
|
|
15
|
-
<a class="wordmark" href="./" aria-label="Webvas home"><span class="mark">W</span> webvas</a>
|
|
16
|
-
<div class="top-meta"><span class="live-dot"></span><span>Ruby graphics, in the browser</span><a href="https://github.com/rbgfx/webvas">GitHub ↗</a></div>
|
|
17
|
-
</header>
|
|
18
|
-
<main>
|
|
19
|
-
<section class="intro">
|
|
20
|
-
<h1>Write Ruby.<br><span>Watch it move.</span></h1>
|
|
21
|
-
<p class="lede">A quiet workspace for sketches, pixels, and shaders.<br>Runs in a dedicated browser worker.</p>
|
|
22
|
-
</section>
|
|
23
|
-
<section class="workbench" aria-label="Ruby sketch editor">
|
|
24
|
-
<div class="toolbar">
|
|
25
|
-
<div class="file-label"><span class="file-icon">◈</span><span>sketch.rb</span><span class="dirty" id="dirty" aria-hidden="true">●</span></div>
|
|
26
|
-
<label class="example-select"><span class="sr-only">Load an example</span>
|
|
27
|
-
<select id="examples"><option value="">Examples</option></select>
|
|
28
|
-
</label>
|
|
29
|
-
<div class="toolbar-actions">
|
|
30
|
-
<button class="quiet-button" id="share" type="button">Share</button>
|
|
31
|
-
<button class="quiet-button" id="download" type="button">Download .rb</button>
|
|
32
|
-
<button class="quiet-button" id="download-html" type="button">Download HTML</button>
|
|
33
|
-
<button class="run-button" id="run" type="button"><span aria-hidden="true">▶</span> Run <kbd>⌘ ↵</kbd></button>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
<div class="panes">
|
|
37
|
-
<div class="editor-pane">
|
|
38
|
-
<div class="pane-heading"><span>EDITOR</span><span>Ruby 4.0 · UTF-8</span></div>
|
|
39
|
-
<label class="sr-only" for="source">Ruby source code</label>
|
|
40
|
-
<textarea id="source" spellcheck="false" autocapitalize="off" autocomplete="off" autocorrect="off"></textarea>
|
|
41
|
-
</div>
|
|
42
|
-
<div class="stage-pane">
|
|
43
|
-
<div class="pane-heading"><span>CANVAS</span><span id="canvas-size">480 × 320</span></div>
|
|
44
|
-
<div class="stage-wrap" id="stage">
|
|
45
|
-
<div class="loading" id="loading"><span class="spinner" aria-hidden="true"></span><span id="loading-label">Starting Ruby runtime</span></div>
|
|
46
|
-
</div>
|
|
47
|
-
<div class="console" aria-live="polite" aria-atomic="true"><span class="console-dot"></span><span id="status">Preparing the drawing machine…</span></div>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
<footer class="workbench-foot"><span>Runs in a dedicated worker</span><span>Ctrl / ⌘ + Enter to run</span><span id="performance-state">Frame timing warming up</span><span id="runtime-state">WASM loading</span></footer>
|
|
51
|
-
</section>
|
|
52
|
-
<p class="note"><span>✳</span> Source is sent only to the local worker. Share links contain compressed source text.</p>
|
|
53
|
-
</main>
|
|
54
|
-
<script type="module" src="./app.js"></script>
|
|
55
|
-
</body>
|
|
56
|
-
</html>
|