diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock
new file mode 100644
index 0000000..c7c5ae6
--- /dev/null
+++ b/.claude/scheduled_tasks.lock
@@ -0,0 +1 @@
+{"sessionId":"dca9796d-8a4b-4077-8999-f6dfc6f287a1","pid":12539,"procStart":"661839","acquiredAt":1780643072550}
\ No newline at end of file
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 967db2f..233ab4e 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -125,7 +125,44 @@
"Bash(python3 glow.py)",
"Bash(python3 dense.py)",
"Bash(montage g_bloom.png g_legacy.png -tile 2x1 -geometry +2+2 g_cmp125.png)",
- "Bash(montage g_bloom.png g_legacy.png -tile 2x1 -geometry +2+2 g_cmp_fold.png)"
+ "Bash(montage g_bloom.png g_legacy.png -tile 2x1 -geometry +2+2 g_cmp_fold.png)",
+ "Bash(emcc --version)",
+ "Read(//usr/lib/**)",
+ "Bash(command -v emcc)",
+ "Bash(echo \"PATH emcc: $\\(command -v emcc || echo MISSING\\)\")",
+ "Bash(timeout 8 bash -c 'curl -sSI https://github.com 2>&1 | head -3 || echo \"NO-CURL\"; echo \"---\"; ping -c1 -W2 github.com 2>&1 | head -2')",
+ "Bash(echo \"native run exit=$? \\(124=healthy\\)\")",
+ "Bash(curl -s -o /dev/null -w 'HTTP %{http_code} type=%{content_type} bytes=%{size_download}\\\\n' https://staging.frostwarning.com/vectorgons/__TRACKED_VAR__)",
+ "Bash(curl -s https://staging.frostwarning.com/vectorgons/index.html)",
+ "Bash(curl -sI https://staging.frostwarning.com/vectorgons/index.html)",
+ "Bash(curl -s -D - -o /dev/null https://staging.frostwarning.com/vectorgons/index.html)",
+ "Bash(convert test.png -resize 300% test_big.png)",
+ "Bash(cp test.png test_big.png)",
+ "Bash(convert test.png -crop 380x762+360+0 +repage -resize 170% col_morph.png)",
+ "Bash(convert test.png -crop 400x762+700+0 +repage -resize 170% col_dyn.png)",
+ "Bash(python3 winshot.py)",
+ "Bash(DISPLAY=:0 VG_WIN=1 timeout 30 wine vgtest.exe)",
+ "Bash(echo \"wine exit=$?\")",
+ "Bash(convert vgwin_out.ppm -crop 400x762+700+0 +repage -resize 170% win_dyn.png)",
+ "Bash(rm -rf /tmp/vgwin /tmp/vgweb/vg.c /tmp/vgweb/web /tmp/vgweb/*.png)",
+ "Bash(rm -f /tmp/pptr/*.png)",
+ "Bash(xargs -n1 basename)",
+ "Bash(apt-cache policy *)",
+ "Bash(apt-get install *)",
+ "Bash(xa --version)",
+ "Bash(c1541 -h)",
+ "Bash(cl65 --version)",
+ "Bash(dpkg-deb -x cc65_2.19-2_amd64.deb cc65root)",
+ "Bash(/tmp/cc65dl/cc65root/usr/bin/cl65 --version)",
+ "Bash(x64sc --help)",
+ "Bash(pkill -f x64sc)",
+ "Bash(grep -v '^$')",
+ "Bash(timeout 16 bash -c ' *)",
+ "Bash(export CC65_HOME=/tmp/cc65dl/cc65root/usr/share/cc65)",
+ "Bash(DISPLAY=:0 timeout 60 firefox --headless --window-size=1920,1080 --screenshot /tmp/fsshot.png file:///tmp/fstest.html)",
+ "Bash(grep -oE 'getFramebufferSize[^,;]{0,120}' web/vectorgons.js)",
+ "Bash(grep -oE 'onCanvasResize[^,;{]{0,60}|setWindowSize[^,;{]{0,80}' web/vectorgons.js)",
+ "Bash(DISPLAY=:0 timeout 60 firefox --headless --window-size=1920,1080 --screenshot /tmp/fsshot2.png file:///tmp/fstest2.html)"
]
}
}
diff --git a/Makefile b/Makefile
index 82ad284..95631ab 100644
--- a/Makefile
+++ b/Makefile
@@ -35,7 +35,25 @@ screensaver: $(SRC)
$(WINCC) $(WINFLAGS) -DSCREENSAVER -I$(GLFWDIR)/include -o $(TARGET).scr $(SRC) \
-L$(GLFWDIR)/lib-mingw-w64 $(WINLIBS)
-clean:
- rm -f $(TARGET) $(TARGET).exe $(TARGET).scr
+# --- WebAssembly / browser build (Emscripten) ------------------------------
+# Produces vectorgons.js + vectorgons.wasm (load from an HTML page; see
+# web/index.html). Legacy-GL emulation maps the fixed-function + immediate-mode
+# rendering onto WebGL; GLFW3 is emulated; the VBO/FBO entry points are resolved
+# at run time, so GL_ENABLE_GET_PROC_ADDRESS is required. Run:
+# source /path/to/emsdk/emsdk_env.sh && make web
+# Must be served over HTTP (WebAssembly will not load from file://).
+EMCC ?= emcc
+EMFLAGS = -O2 -std=c11 -sUSE_GLFW=3 -sLEGACY_GL_EMULATION=1 -sGL_UNSAFE_OPTS=0 \
+ -sGL_ENABLE_GET_PROC_ADDRESS=1 -sALLOW_MEMORY_GROWTH=1 \
+ -sINITIAL_MEMORY=67108864 -sEXIT_RUNTIME=0
-.PHONY: run windows screensaver clean
+web: web/vectorgons.js
+
+web/vectorgons.js: $(SRC)
+ mkdir -p web
+ $(EMCC) $(EMFLAGS) $(SRC) -o web/vectorgons.js
+
+clean:
+ rm -f $(TARGET) $(TARGET).exe $(TARGET).scr web/vectorgons.js web/vectorgons.wasm
+
+.PHONY: run windows screensaver web clean
diff --git a/README.md b/README.md
index e6f4bc7..69d8aad 100644
--- a/README.md
+++ b/README.md
@@ -157,6 +157,99 @@ To install on Windows: right-click `vectorgons.scr` → **Install** (or copy it
`C:\Windows\System32` and choose it under *Settings → Personalization → Lock
screen → Screen saver*). Like the `.exe`, it needs only Windows system DLLs.
+## Browser (WebAssembly)
+
+The same source compiles to WebAssembly with [Emscripten](https://emscripten.org),
+which maps the GLFW3 windowing and the legacy/immediate-mode OpenGL onto WebGL
+(`-sLEGACY_GL_EMULATION`). The retained-mode VBO geometry, the FBO bloom, and the
+frustum culling all run unchanged; the browser-specific bits (GLU helpers, the
+`requestAnimationFrame` main loop, and the WebGL-incompatible `double`/`glReadBuffer`
+calls) are guarded behind `#ifdef __EMSCRIPTEN__`.
+
+```sh
+source /path/to/emsdk/emsdk_env.sh # put emcc on PATH
+make web # -> web/vectorgons.js + web/vectorgons.wasm
+```
+
+Serve the `web/` directory over HTTP (WebAssembly will **not** load from a
+`file://` URL) and open `index.html`:
+
+```sh
+cd web && python3 -m http.server 8000 # then visit http://localhost:8000/
+```
+
+`web/index.html` is a ready-made page. To embed in your own page, drop a focusable
+canvas with `id="canvas"`, point Emscripten's `Module` at it, and load the script:
+
+```html
+
+
+
+```
+
+Serve `vectorgons.js`, `vectorgons.wasm`, and `vectorgons-boot.js` from the same
+directory. Click the canvas to give it keyboard focus; all the usual controls
+then work, and `F` requests browser fullscreen. (Settings are not persisted in
+the browser.)
+
+### Content-Security-Policy
+
+WebAssembly needs a couple of CSP allowances. If your server sends a strict CSP
+(symptom: the canvas appears but nothing runs, and the console shows
+`both async and sync fetching of the wasm failed`), the page's `script-src` must
+include **`'wasm-unsafe-eval'`** (required to instantiate any WASM — there is no
+nonce/hash alternative) and **`connect-src`** must allow fetching the `.wasm`
+(e.g. `'self'`). Loading the `Module` config from `vectorgons-boot.js` (rather
+than an inline `
+
+