@nippur72/crt-emulator
v1.5.0
Published
CRT screen emulation (old color TV set) class written in TypeScript and WebGL
Maintainers
Readme
@nippur72/crt-emulator
A WebGL-based CRT (Cathode-Ray Tube) screen emulator library used for @nippur72 web-based emulators.
The fragment shader emulates screen curvature, scanlines, an RGB phosphor shadow mask, and a composite-video chroma path: band-limited colour bleed plus luma-to-chroma crosstalk (the rainbow/colour fringing typical of 8-bit computers such as the C64 connected via composite video).
Usage
import { CRTEmulator } from "@nippur72/crt-emulator";
const crt = new CRTEmulator(canvas);
crt.init();
// every frame
crt.render(320, 200, false, imageData, {
chromaBleed: 2.0, // chroma smear radius in source pixels (0 = none)
chromaPhase: 0.5, // subcarrier cycles per source pixel (0.5 = NTSC, 0.625 = PAL)
chromaCrosstalk: 0.5, // luma -> chroma leakage, i.e. fringing strength (0..1)
});All options are optional and merged with sensible defaults, see CRTEmulatorOptions
in src/crt_emulation.ts.
Composite chroma options
chromaBleed(default2.0) — radius in source pixels over which colour detail is smeared along the scanline. A TV's chroma path has far less bandwidth than its luma path, so saturated colours bleed ~1.5–3 pixels at 320-wide output while luminance stays sharp.0disables the bleed (fringing alone can still be enabled viachromaCrosstalk).chromaPhase(default0.5) — colour-subcarrier phase advance in cycles per source pixel. This is the ratio of subcarrier to pixel clock and controls the hue of the crosstalk fringes:0.5for NTSC (C64 US: 3.58 MHz / 7.16 MHz),0.625for PAL (C64 EU: 4.43 MHz / 7.09 MHz),0for a plain symmetric smear with no directional fringing.chromaCrosstalk(default0.5) — luma-to-chroma leakage (0..1). Sharp luminance edges contain energy at the subcarrier frequency; a real TV decodes this as spurious colour, producing the alternating rainbow fringes on text and high-contrast edges.
Notes on accuracy
The composite chroma path is processed in linear light (the shader's working space), while hardware composite decoders operate on the gamma-encoded signal. This makes the bleed/fringe tint and spread subtly differ from real hardware; the overall character is preserved.
The chroma low-pass kernel grows continuously from zero effect at chromaBleed = 0,
so small values produce proportionally subtle bleeding instead of jumping straight to
a full smear.
Additional effects
All optional, default off. When any of them is non-zero the emulator switches to a multipass render path (offscreen framebuffers); at defaults it stays on the fast single-pass path.
convergence(default0) — beam convergence error in output pixels: red is sampled slightly left and blue slightly right of green, producing colour fringes on sharp edges like a misconverged colour TV. Typical values 0.3–1.vignette(default0) — corner darkening (0..1), approximating tube edge falloff. Try0.2–0.4.jitter(default0) — horizontal sync jitter amplitude in output pixels. Each scanline is displaced by a pseudo-random amount that changes over time, mimicking imperfect sync on marginal signals. Keep sub-pixel (< 1).persistence(default0, up to0.95) — phosphor afterglow. Bright areas fade out over several frames instead of vanishing instantly; decay is frame-rate independent. Values around0.8–0.9give a subtle ghosting trail.bloom(default0, up to1) — soft glow around bright areas (beam spread + glass reflections), computed with a bright-pass + separable blur at quarter resolution. Start around0.3.maskType(default"slot") — phosphor mask geometry:"slot"(staggered RGB slots, as in late colour TVs/monitors),"grille"(continuous vertical stripes, aperture grille) or"delta"(per-row rotating RGB order, delta-gun shadow mask of early colour TV sets).
crt.render(320, 200, false, imageData, {
convergence: 0.6,
vignette: 0.25,
jitter: 0.3,
persistence: 0.85,
bloom: 0.3,
maskType: "delta",
});Interactive Control Panel
You can open a floating, draggable parameters window at any time by calling window.crt_emulator() in the console or from code:
// Opens the draggable parameters panel
window.crt_emulator();
// Or with initial values and a real-time change callback:
window.crt_emulator(currentOptions, (newOptions) => {
// update rendering options
});- Draggable: Drag the title bar to reposition anywhere on screen.
- Always on top: Floats above your application content.
- Console output: When closed, the current parameter object is printed to the console as JSON for easy copy-pasting.
Demo
Run npm run demo from the repository root and open http://localhost:8080/demo/.
It serves the repo (the demo imports the built library from dist/) and renders a
C64-like 320×200 test frame with interactive sliders for all composite-chroma settings.
Rebuild first with npm run build after changing the shader.
You can also paste a screen copied from an emulator (e.g. a VICE screenshot):
focus the page and press Ctrl+V — the image is used as the emulator source,
and the canvas resizes to keep its aspect ratio. "reset demo frame" restores the
built-in test pattern.
License
MIT
