Post-processing with spryte
Two features make most screen effects possible: render targets (draw into an offscreen image) and custom shaders (change how pixels come out). spryte has both, in its immediate-mode style.
Render targets
const scene = draw.createTarget(320, 180); // pixels
function frame() {
draw.begin("#000", 320, 180, scale);
draw.into(scene, () => {
// Inside: units are the target's pixels; transform, clips and alpha start fresh.
drawWorld(draw);
}, "#102030"); // clear colour; omit for transparent, null to keep contents
draw.sprite(scene, 0, 0, { origin: [0, 0] }); // draw it like any image
draw.end();
}into()restores the outer transform, clips, alpha and destination afterwards, even if your function throws. It nests, so a target can be drawn into another.- The blend mode and an active shader carry into the target, and stay as your function left them.
begin()resets both each frame. - Pass
nullas the clear colour to draw on top of what's already there: that's how trails and motion blur accumulate. - Drawing a target into itself throws. So does a target larger than the GPU allows (
MAX_TEXTURE_SIZE). Sizes are whole pixels. - Free a target you no longer need with
draw.disposeTarget(t). After a lost GPU context, targets come back blank.
Custom shaders
A shader is a fragment effect: you write effect(), which returns the colour of a pixel. source(uv) samples the image being drawn.
const crt = draw.createShader(`
uniform float time;
vec4 effect(vec4 color, vec2 uv) {
float scan = 0.85 + 0.15 * sin(uv.y * 900.0);
vec2 wobble = vec2(sin(uv.y * 30.0 + time * 4.0) * 0.0015, 0.0);
return source(uv + wobble) * color * vec4(vec3(scan), 1.0);
}
`);
crt.set("time", t);
draw.withShader(crt, () => draw.sprite(scene, 0, 0, { origin: [0, 0] }));coloris the draw's tint and alpha, premultiplied: return premultiplied colour too (rgb already multiplied by alpha), andsource(uv) * colordoes the right thing.- Uniforms are floats or vectors (
set("glow", [1, 0.8, 0.3])).set()affects only draws made after it, so you can change a uniform between two draws in the same scope. - Shaders compile on first use. A typo throws with the driver's message, and the line numbers refer to your source. A shader that fails to compile leaves the renderer exactly as it was.
- Shaders are WebGL2 only. On the Canvas 2D fallback, draws inside
withShaderhappen without the effect, with one console warning.
Putting it together: glow
Draw the bright things into a small target, then add it back on top, scaled up and blended additively:
const glow = draw.createTarget(160, 90); // half resolution: cheap, and naturally soft
draw.into(glow, () => {
draw.scale(0.5);
draw.sprites(spark, sparks, live, { tint: "#ffd36b" });
});
drawWorld(draw);
draw.blend("add");
draw.sprite(glow, 0, 0, { origin: [0, 0], scaleX: 2, scaleY: 2, smooth: true });
draw.blend("normal");smooth: true on the upscale blurs the half-resolution glow for free. For a stronger blur, draw the target through a blur shader.
Checked on a real GPU
spryte's browser tests draw scenes directly and through a target, and compare every pixel (they match on both backends). They also check that an identity shader changes nothing, that uniforms apply in order, and that compile errors point at your line. These features are new in spryte 0.1, so if you hit something odd, please open an issue.
See also: Using spryte on its own, and the API reference.