canvaskit-wasm 0.39 build 2026-04-29

SkSL runtime effects

RuntimeEffect lets you write a shader in SkSL — Skia's GLSL-flavored shading language — and use it the same way you use any other Shader. This page is a working reference for the language as Skia exposes it through CanvasKit.

The pattern is always the same:

  1. Write SkSL source as a string.
  2. CK.RuntimeEffect.Make(source) compiles it.
  3. effect.makeShader(uniforms) produces a Shader instance.
  4. paint.setShader(shader) and draw.

Minimal program

const sksl = ` half4 main(float2 xy) { return half4(xy.x / 420.0, xy.y / 256.0, 0.0, 1.0); } `; const effect = CK.RuntimeEffect.Make(sksl); const paint = new CK.Paint(); const shader = effect.makeShader([]); paint.setShader(shader); canvas.drawRect(CK.LTRBRect(0, 0, 420, 256), paint); surface.flush(); shader.delete(); paint.delete(); effect.delete();

Every SkSL fragment shader has the same entry point: half4 main(float2 xy). xy is the pixel coordinate in the destination's local space — not normalized. Return the color as half4(r, g, b, a) with components in [0,1].

Types

SkSL type What it is Notes
float 32-bit float The default scalar.
half 16-bit float Color/uniform values; preferred for the return type.
int 32-bit signed int Loop counters, indices.
bool boolean true/false.
float2, float3, float4 2/3/4-component float vector Arithmetic is component-wise.
half2, half3, half4 half vectors Use in color math.
float2x2, float3x3, float4x4 column-major matrices Multiplication is mat * vec.

Vectors swizzle like GLSL: v.xy, v.rgb, v.bgra. Construction: float3(x, y, z), half4(rgb, a) (broadcasting), float4(0) (all zero).

Built-in functions

The full GLSL standard library is available. The ones you'll reach for:

  • Trig: sin, cos, tan, asin, acos, atan, atan2.
  • Exp / log: exp, log, exp2, log2, pow, sqrt, inversesqrt.
  • Common: abs, sign, floor, ceil, fract, mod, min, max, clamp, mix, step, smoothstep.
  • Geometric: length, distance, dot, cross, normalize, reflect, refract.
  • Vector relational: lessThan, greaterThan, equal, any, all.

Operate component-wise on vectors: mix(a, b, t) works for floats and float3s.

Uniforms

Declare at the top, pass values to makeShader:

const sksl = ` uniform float u_time; uniform float2 u_resolution; uniform float3 u_tint; half4 main(float2 xy) { float2 uv = xy / u_resolution; float ring = sin(length(uv - 0.5) * 30.0 - u_time * 4.0); return half4(half3(u_tint) * (0.5 + 0.5 * ring), 1.0); } `; const effect = CK.RuntimeEffect.Make(sksl); const paint = new CK.Paint(); let t = 0; loop(() => { t += 0.016; // Order in the array matches order in the SkSL source: u_time, u_resolution, u_tint. const shader = effect.makeShader([t, w, h, 0.95, 0.5, 0.2]); paint.setShader(shader); canvas.drawRect(CK.LTRBRect(0, 0, w, h), paint); shader.delete(); surface.flush(); });

Rules:

  • The flat number[] you pass must be the concatenated uniform values, in source order. u_time (1 float) + u_resolution (2 floats) + u_tint (3 floats) = 6 floats.
  • effect.getUniformFloatCount() tells you the total length expected.
  • effect.getUniformName(i) and effect.getUniform(i) introspect at runtime — useful when a shader gets edited live.

Coordinate space

xy arrives in the local coordinate space of the geometry the paint is drawn against. If you draw a 1000×1000 rect, xy ranges over [0, 1000]. To get a 0–1 normalized UV, divide by your known resolution (passed as a uniform).

Rotations and translations applied to the canvas don't affect xy inside the shader — Skia samples in the geometry's pre-transform space. Pass a localMatrix to makeShader if you need to remap.

Child shaders

A SkSL program can sample other shaders. Declare them with uniform shader name; and call name.eval(coord):

Error: Line 1: Unexpected identifier

makeShaderWithChildren(uniforms, children) binds each uniform shader declaration to a Shader from the children array, in declaration order. Common children: image samplers, gradients, even other runtime shaders.

Control flow

if / else, for, ternary ?: all work. No recursion, no while with non-constant bounds (loops must have a static upper bound the SkSL compiler can unroll). Functions:

float sdCircle(float2 p, float r) {
  return length(p) - r;
}

half4 main(float2 xy) {
  float d = sdCircle(xy - float2(210.0, 128.0), 60.0);
  float a = smoothstep(2.0, -2.0, d);
  return half4(half3(0.2, 0.6, 0.9) * a, a);
}

Compile errors

Make(source, errorCallback) returns null on failure and calls the callback with a diagnostic string:

const effect = CK.RuntimeEffect.Make(badSource, (msg) => {
  console.error('SkSL error:', msg);
});
if (!effect) {
  // bail out — no shader to install
}

The error format is similar to GLSL's: error: <line>: <message>. Common ones in practice:

  • Forgetting the half4 return type (using float4 works in many drivers but is non-portable).
  • Calling a function before it's declared (SkSL is single-pass; declare or forward-declare first).
  • Mismatched vector dimensions in arithmetic.

What SkSL is not

  • Not GLSL: no gl_FragCoord, no varying/attribute, no preprocessor #version directive.
  • Not WGSL: different type names, different builtins.
  • No textures, only child shaders. To sample an image, build an image shader (image.makeShaderOptions(...)) and pass it as a child.
  • No multiple render targets, no compute. RuntimeEffect produces a single half4 per pixel.

See also