BLIT386BLIT386

Skill: Add a CRT or post-process effect

Stack fullscreen post-process effects (CRT curvature, scanlines, bloom, glitch) on top of the finished frame, which runs on WebGPU only. Use for a retro CRT...

Written for blit386 1.8.0. The rest of blit386.dev documents the newest engine, so an API you find there may be missing from a game pinned to 1.8.0.

Stack fullscreen effects (CRT curvature, scanlines, bloom, glitch) on top of the finished frame. These run on WebGPU only.

When to use

Use for a retro CRT look, screen glitches, bloom or glow, or any whole-screen filter.

Step 1: a bigger output buffer

Display-tier effects (curvature, scanlines, mask, bloom) need an output buffer larger than the logical screen, set in configure():

configure() {
    return {
        displaySize: new Vector2i(320, 240), // you draw here
        drawingBufferSize: new Vector2i(1280, 960), // effects render here (required)
    };
}

Step 2: guard the software fallback, then add effects

Post-process throws on the Canvas 2D fallback, so check the active backend first:

async init() {
    // ...palette and setup...
    if (BT.activeBackend === 'webgpu') {
        this.crt = BT.preset.crtPipBoy(); // a ready-made array of CRT effects
        for (const fx of this.crt) BT.effectAdd(fx);
    }
    return true;
}

Turn it off again (guard the same way - this.crt is never set on the software fallback):

if (this.crt) {
  for (const fx of this.crt) BT.effectRemove(fx);
}
// or remove everything at once:
BT.effectClear();

Step 3: respect reduced motion (engine 1.7.0+)

Some players ask their system for less motion because heavy movement makes them dizzy or unwell. The moving effects are the first thing to tone down: Flicker, Interference, RollLine, Noise, and PixelGlitch all animate, and a flickering, rolling CRT is exactly what that setting exists to suppress. Static curvature, scanlines, and vignette are fine to leave on.

import { BT, Flicker, Interference, Noise, RollLine } from 'blit386';

async init() {
    if (BT.activeBackend === 'webgpu') {
        this.crt = BT.preset.crtPipBoy();
        this.applyMotionPreference();
    }
    return true;
}

applyMotionPreference() {
    if (!this.crt) return;

    // Keep the still parts of the look, drop the ones that move.
    // crtPipBoy() ships four animating effects; the rest (curvature, aberration, scanlines,
    // mask, bloom) hold still and are fine to leave on.
    const moving = [Flicker, RollLine, Interference, Noise];

    for (const fx of this.crt) BT.effectRemove(fx);
    for (const fx of this.crt) {
        if (BT.isReducedMotionPreferred && moving.some((Kind) => fx instanceof Kind)) continue;
        BT.effectAdd(fx);
    }
}

// Optional. Runs if the player changes the setting while the game is open.
onReducedMotionChange() {
    this.applyMotionPreference();
}

BT.isReducedMotionPreferred is a getter (no parentheses). Screen shake you wrote yourself deserves the same treatment - clamp its amplitude to 0 when the flag is set.

Key calls

  • BT.effectAdd(effect) / BT.effectRemove(effect) / BT.effectClear() - methods.
  • BT.isReducedMotionPreferred (getter, engine 1.7.0+) - true when the player asked for less motion; optional onReducedMotionChange(prefersReduced) on your game class fires when that changes mid-session.
  • Presets (functions on BT.preset): BT.preset.crtPipBoy(), BT.preset.amber(), BT.preset.green() - each returns a fresh array of effects.
  • Effect classes you can construct, tweak, then effectAdd: display-tier Scanlines, BarrelDistortion, Bloom, Vignette, RGBMask, ChromaticAberration, Noise, Flicker, Interference, RollLine; pixel-tier PixelGlitch, PixelMosaic.
import { Scanlines } from 'blit386';
const lines = new Scanlines();
lines.strength = 0.4;
BT.effectAdd(lines);

Notes

  • Always gate effects behind BT.activeBackend === 'webgpu' so the game still runs on the software fallback. Keep starter games effect-free.
  • Gate on BT.activeBackend, never on BT.requestedBackend. requestedBackend is the backend you asked for, and it still says 'webgpu' even after the engine has quietly fallen back to Canvas 2D - so a guard built on it passes on exactly the machines it was meant to protect, and BT.effectAdd throws there.
  • Some effects animate from a time field - set fx.time = BT.timeSeconds each frame.
  • Hold onto effect instances and reuse them; rebuilding every toggle re-allocates GPU pipelines.
  • Full effect reference lives in the engine repo (linked from AGENTS.md); the local docs do not cover every effect.

On this page