webvas 0.3.0 → 0.3.2

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/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>