Folder: glsl-tauri-v2-ws-template
Stack: Tauri 2 + HTML/CSS/JavaScript + WebGL 1 + GLSL ES 1.0 + Rust WebSocket relay
Window model: Separate controls and renderer WebViews
This example adds external fragment-shader transport to the two-window raw WebGL architecture. It shows how to:
2727;.frag file in the controls WebView;The default visual remains the original domain-warped fractional Brownian motion study. This project stays focused on external GLSL lifecycle and two-window message flow rather than becoming a general shader editor, compositor, recorder, or media router.
Example 04 loads and replaces external GLSL inside one WebView. Example 05 introduces one architectural variable: the source picker and control state live in one window, while compilation and rendering happen in another.
That separation demonstrates a practical pattern for dedicated output windows. The controls window can remain interactive and scrollable while the canvas window is placed on another display, made fullscreen, or restarted independently.
User selects my-shader.frag
│
▼
controls.js reads text and enforces a 1 MiB limit
│
│ WebSocket JSON
│ { type: "shader", name, source, revision }
▼
Embedded Rust relay on ws://127.0.0.1:2727
│ broadcasts text to the other connected WebView
▼
canvas.js builds a candidate WebGL program
│
├── compile/link succeeds ──► replace active program
│ return accepted result
│
└── compile/link fails ─────► keep previous valid program
return rejection + compiler log
Parameter traffic follows the same relay:
Controls slider
│ coalesced once per animation frame
▼
{ type: "param", name: "zoom", value: 1.5 }
▼
Rust WebSocket relay
▼
Canvas parameter state
▼
Uniform upload on the next render frame
Diagnostics travel in reverse:
Canvas compiler / GPU / FPS / resolution telemetry
▼
Rust relay
▼
Controls diagnostics panel and compiler terminal
The canvas never deletes the active program before a replacement is validated.
The replacement sequence is:
a_position attribute;If any step fails, the candidate resources are deleted and the existing program continues rendering. The renderer shows a non-fatal warning while the complete compiler or linker log is returned to the controls window.
The controls window is the in-session source of truth for:
When the canvas connects, it sends:
{ "type": "state_request", "role": "canvas" }
The controls respond with the complete parameter state, then resend the last accepted shader. If a candidate was pending when the connection was interrupted, the accepted shader is sent first so the renderer has a known-good fallback before the pending candidate is retried.
{ "type": "hello", "role": "controls" }
{ "type": "hello", "role": "canvas" }
{
"type": "param",
"role": "controls",
"name": "distortion",
"value": 0.42
}
{
"type": "state",
"role": "controls",
"params": {
"hue": 180,
"zoom": 1.5,
"speed": 0.5
},
"paused": false,
"resetTime": false,
"reason": "canvas_requested_state"
}
{
"type": "shader",
"role": "controls",
"name": "my-shader.frag",
"source": "precision highp float; ...",
"revision": 3,
"reason": "user_candidate"
}
{
"type": "shader_result",
"role": "canvas",
"accepted": false,
"name": "my-shader.frag",
"revision": 3,
"activeShaderName": "shader.frag",
"fragmentStatus": "Compile failed",
"compilerLog": "[rejected] my-shader.frag\n..."
}
{ "type": "action", "role": "controls", "name": "pause", "value": true }
{ "type": "action", "role": "controls", "name": "fullscreen" }
{
"type": "telemetry",
"role": "canvas",
"runtime": "running",
"sourceStatus": "Active",
"vertexStatus": "Compiled",
"fragmentStatus": "Compiled",
"linkStatus": "Linked",
"activeShaderName": "shader.frag",
"renderer": "Apple GPU",
"resolution": "1760 × 1360",
"fps": 60.0,
"paused": false
}
src/controls.html contains:
src/controls.js reads shader files with the browser File API. It limits source to 1 MiB, stores the last shader accepted by the canvas, tracks a pending candidate by revision, coalesces slider updates with requestAnimationFrame(), and restores state after reconnects.
The default shader.frag is fetched by the controls window so it can be resent to a restarted canvas without requiring the entire application to restart.
src/canvas.html contains the canvas, minimal HUD, a non-fatal shader-rejection notice, and a fatal renderer-error overlay.
src/canvas.js owns:
shader.frag;src-tauri/src/main.rs performs two focused jobs:
tauri::async_runtime::spawn();toggle_canvas_fullscreen, retrieving the configured renderer through Tauri 2’s get_webview_window("canvas") API.Text messages are broadcast to all connected clients except their sender. The existing binary route remains available for later experiments and forwards only to clients identified as canvas.
src-tauri/capabilities/main-capability.json explicitly grants core:default to both WebView labels.
src-tauri/Cargo.toml contains a local workspace boundary:
[workspace]
resolver = "2"
This prevents Cargo from walking upward into an unrelated parent workspace.
glsl-tauri-v2-ws-template/
├── README.md
├── package.json
├── package-lock.json
├── src/
│ ├── controls.html
│ ├── controls.js
│ ├── canvas.html
│ ├── canvas.js
│ ├── shader.frag
│ └── styles.css
└── src-tauri/
├── build.rs
├── Cargo.toml
├── Cargo.lock
├── tauri.conf.json
├── capabilities/
│ └── main-capability.json
├── icons/
└── src/
└── main.rs
cd glsl-tauri-v2-ws-template
npm install
The Tauri CLI is installed locally through devDependencies; no global CLI is required.
npm run dev
Tauri opens:
The embedded relay listens only on local loopback interfaces:
ws://127.0.0.1:2727
ws://[::1]:2727
npm run build
Desktop bundles are written beneath:
src-tauri/target/release/bundle/
Signing, notarization, installer identity, and platform distribution requirements remain the responsibility of the final application.
Replacement fragment shaders must be valid GLSL ES 1.0 and may use the uniforms below:
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_hue;
uniform float u_saturation;
uniform float u_brightness;
uniform float u_zoom;
uniform float u_distortion;
uniform float u_rotate;
uniform float u_complexity;
uniform float u_symmetry;
uniform float u_glow;
uniform float u_invert;
uniform float u_pulse;
varying vec2 vTexCoord;
Unused uniforms are permitted. The renderer skips locations optimized away by the GLSL compiler.
The fixed vertex shader exposes:
attribute vec2 a_position;
varying vec2 vTexCoord;
A replacement fragment shader must define main() and write gl_FragColor.
| Control | Shader role |
|---|---|
Load .frag |
Reads a local text shader and sends it to the canvas for validation |
| Restore Default | Resends the bundled shader.frag |
| Hue Shift | Base HSB hue in degrees |
| Saturation | Color saturation |
| Brightness | Pattern output gain |
| Zoom | UV scale before noise evaluation |
| Speed | Multiplier applied to accumulated shader time |
| Distortion | Strength of the two-sample domain warp |
| Complexity | Number of active fBm octaves, 1–8 |
| Symmetry | Number of radial sectors |
| Glow | Center-weighted brightness gain |
| Invert | Mixes the color with its inverse |
| Pulse | Enables sinusoidal brightness modulation |
| Rotate | Enables time-based UV rotation |
| Key | Action |
|---|---|
Space |
Pause or resume animation |
R |
Reset parameters and renderer time |
O |
Open the shader file picker |
D |
Restore the bundled default shader |
F |
Toggle canvas-window fullscreen |
The canvas window also accepts F for fullscreen.
The controls window shows:
A bad replacement produces a visible warning rather than a blank canvas. The renderer continues with the previous program.
Both windows are WebViews. Rendering uses WebGL 1 and GLSL ES 1.0 through the embedded browser graphics stack; this is intentionally different from Junkpile’s native Rust + wgpu examples.
The canvas uses the global vanilla-JavaScript Tauri API for native fullscreen:
window.__TAURI__.core.invoke('toggle_canvas_fullscreen')
app.withGlobalTauri is enabled in tauri.conf.json, and both window labels are covered by the Tauri 2 capability.
2727 must be available. A second running copy cannot bind the same port.1 MiB.2 to avoid unusually large drawing buffers.shader.frag and verify the old program continues rendering.This project deliberately does not add camera input, recording, media routing, native filesystem commands, native wgpu rendering, or a full integrated development environment. Those concerns belong to later Junkpile examples and applications.