documentation

The shader convention

Every pons shader is a single, self-contained WGSL fragment shader that follows one convention: a fixed uniform layout, one entry point, and a coordinate system that needs one line of correction. Two checks enforce it before anything gets published.

The Params struct

A shader declares exactly one uniform block, named Params, bound at @group(0) @binding(0). Its first three fields are fixed, in this exact order, and every custom parameter the collection declares comes after them as its own f32 field:

the required shapewgsl
struct Params {
  time: f32,
  seed: f32,
  texel: vec2f,
  // your seeded parameters, all f32, in any order after this
}

@group(0) @binding(0) var<uniform> params: Params;
struct Params, field order is fixed
time: f32seconds since the canvas started. Drives the animation loop.
seed: f32a float in [0, 1) unique to this token. Same seed, same look, on any machine.
texel: vec2f1 / width, 1 / height in pixels. Used to correct for aspect ratio.
<your fields>one f32 per declared param, seeded independently within its range.

Entry point and coordinates

The fragment entry point has one fixed signature:

wgsl
@fragment
fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
  // ...
}

uv is top-origin and runs from 0 to 1 on both axes, but it is not corrected for the canvas's aspect ratio. Almost every shader wants to fix that before doing anything else, using texel:

wgsl
let aspect = params.texel.y / params.texel.x;
let p = (uv - 0.5) * vec2f(aspect, 1.0);

From there, p is a square, centered coordinate space regardless of the viewport's shape, which is what almost every shader in pons actually draws in.

What the validator checks

A shader is checked twice before a collection can publish it, once structurally and once for real:

  • It contains the text struct Params and an @fragment entry point.
  • The Params block has time, seed, and texel as members.
  • Every parameter key declared for the collection is also a member of Params. A declared key missing from the struct silently gets ignored at render time, so this is checked before publish rather than discovered later.
  • The shader is actually compiled and rendered for one small frame, server-side, with realistic derived parameter values. This catches anything the structural check can't: a typo in a function name, a type mismatch, a loop with a non-constant bound.

A full annotated example

This is the starter shader shown in the collection studio: a single pulsing ring, with the seed nudging its color and two collection params (scale, speed) tuning its rhythm.

ring.wgslannotated
struct Params {
  time: f32,       // seconds; drives the animation
  seed: f32,        // [0, 1), unique per token
  texel: vec2f,      // 1/width, 1/height, for aspect correction
  scale: f32,        // custom: how tightly the rings pack
  speed: f32,        // custom: how fast it drifts
}

@group(0) @binding(0) var<uniform> params: Params;

// A cheap way to turn one float into a moving RGB color.
fn spectrum(t: f32) -> vec3f {
  return 0.5 + 0.5 * cos(6.28318 * (t + vec3f(0.0, 0.33, 0.67)));
}

@fragment
fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
  // uv is top-origin, [0,1] on both axes, and NOT aspect-corrected.
  // texel.y / texel.x gives you the aspect ratio to fix that:
  let aspect = params.texel.y / params.texel.x;
  let p = (uv - 0.5) * vec2f(aspect, 1.0);

  let t = params.time * params.speed;
  let d = length(p) * params.scale - t;

  // seed shifts the color per token so identical params still look distinct
  let col = spectrum(fract(d + params.seed))
    * smoothstep(0.9, 0.2, length(p));

  return vec4f(col * 0.85, 1.0);
}

Writing shaders that hold up

  • Use params.seed somewhere that visibly changes the image. Two tokens with similar param rolls should still read as different pieces.
  • Animate with params.time, but keep the motion slow. Confident drift reads better than frantic movement on a page full of thumbnails.
  • f32 literals need a decimal point (1.0, not 1), loops need constant bounds, and while(true) is not allowed. WGSL is stricter than GLSL about all three.
  • Near-black backgrounds and additive glow read well on the site's dark pages. Hard color bands and flat rainbow fills tend to look cheap in comparison.