Skip to main content
A runtime shader is your own pixel effect: an SkSL function the API compiles into the three variants the render engines run (GLSL 330 core on the farm, GLSL ES 3.00 in the WebGL build, WGSL in the Rust preview engine). You post the source once, get back an id, and use that id in the effects of a video, image, shape, frame or instance.

The loop

  1. POST /v1/shaders with the source and its params. The answer is 201 with the shader’s id, or 422 shader_compile_error with every error’s line and column.
  2. POST /v1/shaders/{id}/probe renders one frame of it on a full-canvas shape (a PNG preview, not billed). Poll the job with GET /v1/renders/{id}.
  3. Put it in a composition: effects: [{"shader": {"id": "sh_...", "params": {...}}}].
  4. To change the source, post it again: a new source is a new shader with a new id.

Compile a shader

POST /v1/shaders
201
The entry is half4 main(float2 uv), with uv from (0, 0) at the top-left of the element to (1, 1) at its bottom-right. A source that defines Jitter’s half4 jitterMain(float2 uv) compiles in Jitter’s dialect (jitter: true); pass "jitter": true to require it. Do not declare uniforms: everything a source reads comes from the prelude below or from its params, each read as u_<name>. hash is the SHA-256 of "sksl\n" plus the source, the shader’s identity. The variants’ text comes back with GET /v1/shaders/{id}?view=full, beside the source.

Compile errors

422
line and col are 1-based positions in your source. Line 0 marks an error outside it: a param (param 'time': u_time is a prelude name) or a limit. A name the prelude does not have is reported where it is first used (unknown prelude symbol 'u_amount'). The failed shader is stored too, so GET /v1/shaders/{id} keeps its errors; an effect that names it is refused with shader_compile_error.

Use it in an effect

An effect takes either a vocabulary type or a shader, and beside a shader only intensity (static or intensity_keyframes). A shape with fill: "none" and no stroke is a generator: the shader paints the whole box from nothing. On any other element the shader reads the element’s own pixels through sampleSource. A param’s value is one of these or keyframes, [{time, value}] in timeline seconds, linear between and held outside. A param left out takes its declared default; a value outside the declared min and max is refused, as are params the shader does not declare (shader_param_invalid). The render carries the compiled variants, so it never depends on the shader still existing once it is queued.

Limits

The prelude

Functions: sampleSource(float2 uv) (the source, premultiplied), uvToLocal and localToUv (uv into and out of the content box), transformUv(uv, aspect, pivot, translate, scale, angleRad), simplexNoise(float2 p, float2 res, float scale) (in -1 to 1) and Skia’s unpremul. Jitter’s dialect adds jitterSampleSource, jitterUvToLocal, jitterLocalToUv, jitterTransformUv, jitterSimplexPerlinNoise, u_width/u_height and u_source_width/u_source_height as aliases of the same. GET /v1/capabilities serves all of this as the shaders section.

Shared shaders

GET /v1/shaders lists this workspace’s shaders and the shared ones (shared: true), newest first. A shared shader works in any workspace’s effects and cannot be deleted (403). Deleting your own shader leaves renders already queued with it untouched; a document that still names it is refused with shader_not_found at its next render.

Availability

The compiler runs beside the API. A deployment without it answers POST /v1/shaders with 503 shader_compiler_unavailable, and shaders.compiler_available in GET /v1/capabilities is false. Rendering a shader needs the v5 render task: while a deployment does not send it (shaders.renders is false), an effect with a shader is refused with unsupported_feature.