# Web Export Export any SimVX game as a standalone HTML file that runs entirely in the browser. No server required — the game logic runs in [Pyodide](https://pyodide.org/) (CPython compiled to WebAssembly) and renders via WebGPU. Both 2D and 3D games are supported — 3D node usage is auto-detected. ## Quick Start ```bash uv run simvx export web my_game.py \ --output game.html --width 800 --height 600 \ --title "My Game" --root MyGameNode ``` Open `game.html` in Chrome or Edge. The file is fully self-contained — host it on any static site. ## How It Works The export tool bundles everything into a single HTML file: ``` HTML file ├─ Pyodide runtime (loaded from CDN, ~15 MB, cached by browser) ├─ simvx.core engine (Python source, bundled inline) ├─ Game code (Python source, bundled inline) ├─ MSDF font atlas (pre-baked PNG, base64-encoded) ├─ WebGPU renderer (renderer2d.js + WGSL shaders, inlined) └─ Input capture (keyboard, mouse, touch → Python Input singleton) ``` Each frame: 1. JavaScript calls `WebApp.tick(dt)` via Pyodide 2. Python runs the scene tree: `physics_process()` → `process()` → `draw()` 3. `Draw2D` commands are serialized to a compact binary format 4. JavaScript passes the binary data to the WebGPU renderer 5. Four GPU pipelines render: filled shapes, lines, MSDF text, textured quads ## Requirements **Export machine:** Standard SimVX dev environment (freetype-py for atlas generation). **Browser:** WebGPU support required — Chrome 113+, Edge 113+, or Firefox Nightly with `dom.webgpu.enabled`. Safari support is in progress. ## Declaring Dependencies Games that import packages beyond `numpy` (which is always loaded) need to declare them so the export tool includes them in the Pyodide bundle. There are three ways, checked in priority order: ### PEP 723 Inline Script Metadata (single-file games) Add a `# /// script` block at the top of your game file. This is the [standard Python mechanism](https://peps.python.org/pep-0723/) for single-file scripts: ```python # /// script # dependencies = ["pillow>=10.0", "scipy"] # /// from simvx.core import Node class MyGame(Node): ... ``` ### pyproject.toml (project-based games) For games organised as a project with a `pyproject.toml`, declare dependencies in the standard `[project]` table: ```toml [project] name = "my-game" dependencies = [ "pillow>=10.0", "scipy", ] ``` The export tool reads `pyproject.toml` from the same directory as the game file. PEP 723 metadata takes precedence if both are present. ### CLI / API Override Pass additional packages directly, regardless of what the game declares: ```bash uv run simvx export web my_game.py \ --packages requests aiohttp ``` ```python export_web("my_game.py", "game.html", extra_packages=["requests", "aiohttp"]) ``` These are merged with any declared dependencies. Version specifiers and `simvx-*` packages are automatically stripped. ## Command-Line Reference ``` uv run simvx export web [options] ``` | Option | Default | Description | |--------|---------|-------------| | `--output`, `-o` | `game.html` | Output HTML file path | | `--width` | `800` | Engine viewport width | | `--height` | `600` | Engine viewport height | | `--title` | `SimVX` | Browser page title | | `--root` | auto-detect | Root `Node` subclass name | | `--fps` | `60` | Physics tick rate | | `--responsive` | off | Adapt engine viewport to browser window size | | `--packages` | none | Additional Pyodide packages to load | Root class is auto-detected from the first class definition in the game module. Specify `--root` if the file contains multiple classes. ## Python API ```python from simvx.web.export import export_web export_web( "my_game.py", "game.html", width=800, height=600, title="My Game", root_class="MyGameNode", physics_fps=60, extra_packages=["pillow"], ) ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `game_path` | `str \| Path` | required | Path to the game's Python module | | `output` | `str \| Path` | `"game.html"` | Output HTML file path | | `width` | `int` | `800` | Engine viewport width | | `height` | `int` | `600` | Engine viewport height | | `title` | `str` | `"SimVX"` | Browser page title | | `root_class` | `str \| None` | `None` | Root Node subclass (auto-detected if None) | | `physics_fps` | `int` | `60` | Physics tick rate | | `charset` | `str \| None` | `None` | Characters to pre-bake in the MSDF atlas | | `responsive` | `bool` | `False` | Adapt viewport to browser window size | | `pyodide_version` | `str` | `DEFAULT_PYODIDE_VERSION` | Pyodide CDN version (canonical default from `simvx.web.export`) | | `extra_packages` | `list[str] \| None` | `None` | Additional Pyodide packages to load | ## Example: Tic Tac Toe The tictactoe example exports to a 256 KB HTML file (before Pyodide CDN): ```bash uv run simvx export web \ packages/graphics/examples/game_tictactoe/game.py \ --output tictactoe.html --width 400 --height 550 \ --title "Tic Tac Toe" --root TicTacToeGame ``` The game renders identically to the desktop version — same UI widgets, same layout, same input handling. ## Font Atlas Text rendering uses MSDF (Multi-channel Signed Distance Field) font atlases. The export tool: 1. Finds a system font on the export machine 2. Scans your game's string literals to determine which characters are needed 3. Pre-renders the MSDF atlas at export time 4. Embeds the atlas as a base64 PNG in the HTML This eliminates the freetype-py dependency at runtime. If your game generates text dynamically (e.g., user input), ensure the charset covers the expected characters. ## Comparison: Web Export vs Video Streaming SimVX offers two ways to run games in a browser: | | Web Export | Video Streaming (`run_streaming`) | |---|---|---| | **Server** | None (static HTML) | Python server + GPU | | **Rendering** | WebGPU in browser | Vulkan on server, JPEG to browser | | **Latency** | Zero (local) | Network round-trip | | **3D support** | Yes (WebGPU) | Yes (Vulkan) | | **Browser** | WebGPU required | Any modern browser | | **Deployment** | Any static host | Needs running server + GPU | | **Startup** | ~5s (Pyodide load) | Instant | Use **web export** for distributing games on the web. Use **video streaming** when you need broader browser support or server-side computation. ## Audio Web exports get full audio via the Web Audio API. `AudioStreamPlayer`, `AudioStreamPlayer2D`, and `AudioStreamPlayer3D` work unchanged — the same code that runs on Vulkan plays in the browser. The engine swaps `MiniaudioBackend` for `WebAudioBackend` (`packages/web/src/simvx/web/audio/web_backend.py`), which bridges the duck-typed backend interface to a JS-side `AudioBridge`. Behaviour notes: - **Source formats** — procedurally synthesised numpy buffers ship as float32 PCM through the resource channel. File-backed streams (WAV / OGG / MP3 / FLAC) ship as raw bytes and are decoded by the browser's `AudioContext.decodeAudioData`. - **Spatialisation** — distance attenuation, pan, and Doppler run in the player nodes (the same code as desktop). The bridge applies the resulting `(gain, pan, pitch)` per channel via `GainNode` and `StereoPannerNode`. No HRTF — playback matches desktop sample-for-sample. - **Buses** — each `AudioBus` becomes a `GainNode` parented to its `send_to` target. Unlike the desktop backend, **bus volume / mute changes propagate live to playing channels** (matches Godot / Unity HTML5 export). The desktop backend currently bakes bus state at play time; bringing it in line is tracked in `TODO.md`. - **User-gesture gate** — browsers require a user interaction before audio plays (autoplay policy). The first `keydown` / `mousedown` / `touchstart` resumes the `AudioContext`. Sounds triggered before the first gesture are queued (bounded buffer of 32) and replayed on resume. - **Streaming** — `AudioStreamPlayer` with `stream_mode = "streaming"` plays via an `AudioWorkletNode` that consumes 16-bit PCM chunks fed at the source sample rate (44.1 kHz). The bridge falls back to `ScriptProcessorNode` when `AudioWorklet` is unavailable (legacy Safari < 14.1). - **Sample rate** — `AudioContext.sampleRate` is browser-controlled (typically 48 kHz). Static `AudioBuffer`s are auto-resampled at playback. ## Internals — resource channel protocol The browser-side renderer receives two streams from the Pyodide runtime each frame: the **scene binary** (viewports, materials, lights, draw groups) and the **resource channel** (texture / mesh / audio uploads). The resource channel uses a typed TLV wire format so new resource kinds slot in without protocol surgery. See [Resource channel wire format](resource-channel.md) for the full spec, kind registry, Python and JS entry points, and rationale for what stays in-frame vs on the channel. ## Limitations - **WebGPU required** — no Canvas2D or WebGL fallback. - **First load** — Pyodide runtime (~15 MB) is downloaded from CDN on first visit. Subsequent visits use the browser cache. - **Performance** — Python in WebAssembly is ~2-5x slower than native. UI-widget games run at 60fps easily; compute-heavy games may need optimisation. - **No filesystem** — `open()`, `subprocess`, and filesystem operations are not available. - **Pyodide packages only** — declared dependencies must be [available in Pyodide](https://pyodide.org/en/stable/usage/packages-in-pyodide.html). Pure-Python packages work; C-extension packages need Pyodide-specific builds.