-
Notifications
You must be signed in to change notification settings - Fork 1
wgsl play
Web component for rendering WESL/WGSL fragment shaders.
@fragment fn main(@builtin(position) pos: vec4f) -> @location(0) vec4f { let uv = pos.xy / u.resolution; return vec4f(uv, sin(u.time) * 0.5 + 0.5, 1.0); } </script>
npm install wgsl-play<script type="module">import "wgsl-play";</script>
<wgsl-play src="./shader.wesl"></wgsl-play>The component auto-fetches dependencies and starts animating.
wgsl-play renders a fullscreen triangle using a built-in vertex shader and only
accepts fragment shaders. Write a single @fragment function.
WESL extensions are supported (imports, conditional compilation).
Standard uniforms are available via env::u:
import env::u;
@fragment fn main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
let uv = pos.xy / u.resolution;
return vec4f(uv, sin(u.time) * 0.5 + 0.5, 1.0);
}When no @uniforms struct is declared, a default is provided with resolution and time.
Declare a struct with @uniforms to add your own fields with UI controls:
import env::u;
@uniforms struct Params {
@auto resolution: vec2f,
@auto time: f32,
@range(1.0, 20.0, 5.0, 6.0) frequency: f32,
@color(0.2, 0.5, 1.0) tint: vec3f,
@toggle(0) invert: u32,
}
@fragment fn main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
let wave = sin(pos.x * u.frequency + u.time);
var color = wave * u.tint;
if u.invert == 1u { color = 1.0 - color; }
return vec4f(color, 1.0);
}The player fills these automatically each frame. The field name determines
which value is bound (or use @auto(name) when the field name differs):
| Name | Type | Description |
|---|---|---|
resolution |
vec2f |
Canvas size in pixels |
time |
f32 |
Elapsed time in seconds |
delta_time |
f32 |
Delta time since last frame |
frame |
u32 |
Frame count |
mouse_pos |
vec2f |
Pointer position in pixels |
mouse_delta |
vec2f |
Pointer movement since last frame |
mouse_button |
i32 |
Active button: 0=none, 1=left, 2=middle, 3=right |
These generate interactive controls in the player.
Slider for f32 or i32. Step defaults to 0.01 for f32, 1 for i32.
Initial defaults to min.
@range(1.0, 20.0) frequency: f32,
@range(1.0, 20.0, 5.0) frequency: f32, // step=5
@range(1.0, 20.0, 0.5, 5.0) frequency: f32, // step=0.5, initial=5Color picker for vec3f:
@color(0.2, 0.5, 1.0) tint: vec3f,Boolean toggle for u32 (0 or 1). WGSL forbids bool in uniform buffers.
@toggle invert: u32, // default=0
@toggle(1) invert: u32, // default=1Fields without annotations are zero-initialized and settable from JavaScript
via setUniform(). This works before or after compilation.
@uniforms struct Params {
@auto resolution: vec2f,
brightness: f32, // no annotation — set from JS
}const player = document.querySelector("wgsl-play");
player.setUniform("brightness", 0.8);Include shader code inline with a <script type="text/wesl"> tag:
<wgsl-play>
<script type="text/wesl">
import env::u;
@fragment fn main(@builtin(position) pos: vec4f) -> @location(0) vec4f {
let uv = pos.xy / u.resolution;
return vec4f(uv, sin(u.time) * 0.5 + 0.5, 1.0);
}
</script>
</wgsl-play>const player = document.querySelector("wgsl-play");
player.shader = shaderCode;
player.pause();
player.rewind();
player.play();For apps with multiple shader files, use shader-root:
public/
shaders/
utils.wesl # import package::utils
effects/
main.wesl # import super::common
common.wesl
<wgsl-play src="/shaders/effects/main.wesl" shader-root="/shaders"></wgsl-play>Local shader modules referenced via package:: or super::
will be fetched from the web server.
For more control, use wesl-plugin to assemble shaders and libraries at build time.
import shaderConfig from "./shader.wesl?link";
player.project = {
...shaderConfig,
conditions: { MOBILE: isMobileGPU },
constants: { num_lights: 4 }
};| Attribute | Values | Default | Description |
|---|---|---|---|
src |
URL | - | URL to .wesl/.wgsl file |
shader-root |
string | /shaders |
Root path for internal imports |
from |
element ID | - | Source provider element to connect to (e.g., wgsl-edit) |
autoplay |
boolean | true |
Start animating on load |
no-controls |
boolean | - | Hide playback controls |
no-settings |
boolean | - | Hide the uniform controls panel |
fetch-libs |
boolean | true |
Auto-fetch missing npm libraries |
fetch-sources |
boolean | true |
Auto-fetch local .wesl source files via HTTP |
| Property | Type | Description |
|---|---|---|
shader |
string |
Get/set shader source (single-file convenience) |
project |
WeslProject |
Get/set full project config (weslSrc, libs, conditions, constants) |
conditions |
Record<string, boolean> |
Get/set conditions for conditional compilation |
uniforms |
Record<string, number | number[]> |
Current uniform values (readonly) |
isPlaying |
boolean |
Playback state (readonly) |
time |
number |
Animation time in seconds (readonly) |
hasError |
boolean |
Compilation error state (readonly) |
errorMessage |
string | null |
Error message (readonly) |
| Method | Description |
|---|---|
play() |
Start/resume animation |
pause() |
Pause animation |
rewind() |
Reset to t=0 |
setUniform(name, value) |
Set a uniform value programmatically |
showError(message) |
Display error (empty string clears) |
| Event | Detail | Description |
|---|---|---|
compile-error |
{ message: string } |
Shader compilation failed |
compile-success |
- | Shader compiled successfully |
init-error |
{ message: string } |
WebGPU initialization failed |
playback-change |
{ isPlaying: boolean } |
Play/pause state changed |
uniforms-layout |
AnnotatedLayout |
Fired after each compile with layout metadata |
wgsl-play {
width: 512px;
height: 512px;
}
wgsl-play::part(canvas) {
image-rendering: pixelated;
}